Add interactive view above status bar.
Adds an invisible view layered above status bar to forward input through
the existing handleCaptionThroughStatusBar logic. This reduces our
reliance on a global input monitor and also fixes an issue where desktop
mode could be invoked by dragging down in handle coordinates even while
notification shade is active.
Bug: 349135068
Bug: 341997116
Test: manual
Flag: com.android.window.flags.enable_additional_windows_above_status_bar
Change-Id: I94120c64c7f359661fca632df0251989cd93fa07
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index a91edaa..de901b5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -69,6 +69,7 @@
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener
+import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener
import com.android.wm.shell.draganddrop.DragAndDropController
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
@@ -1334,33 +1335,36 @@
*
* @param taskInfo the task being dragged.
* @param y height of drag, to be checked against status bar height.
+ * @return the [IndicatorType] used for the resulting transition
*/
fun onDragPositioningEndThroughStatusBar(
inputCoordinates: PointF,
taskInfo: RunningTaskInfo,
- ) {
- val indicator = getVisualIndicator() ?: return
+ ): IndicatorType {
+ val indicator = getVisualIndicator() ?: return IndicatorType.NO_INDICATOR
val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
when (indicatorType) {
- DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> {
- val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+ IndicatorType.TO_DESKTOP_INDICATOR -> {
+ val displayLayout = displayController.getDisplayLayout(taskInfo.displayId)
+ ?: return IndicatorType.NO_INDICATOR
if (Flags.enableWindowingDynamicInitialBounds()) {
finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo))
} else {
finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout))
}
}
- DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR,
- DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> {
+ IndicatorType.NO_INDICATOR,
+ IndicatorType.TO_FULLSCREEN_INDICATOR -> {
cancelDragToDesktop(taskInfo)
}
- DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
+ IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
requestSplit(taskInfo, leftOrTop = true)
}
- DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
+ IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
requestSplit(taskInfo, leftOrTop = false)
}
}
+ return indicatorType
}
/** Update the exclusion region for a specified task */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 3f0e875..2570372 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -36,6 +36,8 @@
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.compatui.AppCompatUtils.isTopActivityExemptFromDesktopWindowing;
import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR;
+import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR;
+import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
@@ -327,7 +329,8 @@
if (decoration == null) return;
final RunningTaskInfo oldTaskInfo = decoration.mTaskInfo;
- if (taskInfo.displayId != oldTaskInfo.displayId) {
+ if (taskInfo.displayId != oldTaskInfo.displayId
+ && !Flags.enableAdditionalWindowsAboveStatusBar()) {
removeTaskFromEventReceiver(oldTaskInfo.displayId);
incrementEventReceiverTasks(taskInfo.displayId);
}
@@ -391,7 +394,8 @@
decoration.close();
final int displayId = taskInfo.displayId;
- if (mEventReceiversByDisplay.contains(displayId)) {
+ if (mEventReceiversByDisplay.contains(displayId)
+ && !Flags.enableAdditionalWindowsAboveStatusBar()) {
removeTaskFromEventReceiver(displayId);
}
// Remove the decoration from the cache last because WindowDecoration#close could still
@@ -523,6 +527,9 @@
}
} else if (id == R.id.split_screen_button) {
decoration.closeHandleMenu();
+ // When the app enters split-select, the handle will no longer be visible, meaning
+ // we shouldn't receive input for it any longer.
+ decoration.disposeStatusBarInputLayer();
mDesktopTasksController.requestSplit(decoration.mTaskInfo);
} else if (id == R.id.open_in_browser_button) {
// TODO(b/346441962): let the decoration handle the click gesture and only call back
@@ -659,13 +666,38 @@
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
final RunningTaskInfo taskInfo = decoration.mTaskInfo;
if (DesktopModeStatus.canEnterDesktopMode(mContext)
- && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
- return false;
+ && !taskInfo.isFreeform()) {
+ return handleNonFreeformMotionEvent(decoration, v, e);
+ } else {
+ return handleFreeformMotionEvent(decoration, taskInfo, v, e);
}
+ }
+
+ private boolean handleNonFreeformMotionEvent(DesktopModeWindowDecoration decoration,
+ View v, MotionEvent e) {
+ final int id = v.getId();
+ if (id == R.id.caption_handle) {
+ if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ // Caption handle is located within the status bar region, meaning the
+ // DisplayPolicy will attempt to transfer this input to status bar if it's
+ // a swipe down. Pilfer here to keep the gesture in handle alone.
+ mInputManager.pilferPointers(v.getViewRootImpl().getInputToken());
+ }
+ handleCaptionThroughStatusBar(e, decoration);
+ final boolean wasDragging = mIsDragging;
+ updateDragStatus(e.getActionMasked());
+ // Only prevent onClick from receiving this event if it's a drag.
+ return wasDragging;
+ }
+ return false;
+ }
+
+ private boolean handleFreeformMotionEvent(DesktopModeWindowDecoration decoration,
+ RunningTaskInfo taskInfo, View v, MotionEvent e) {
+ final int id = v.getId();
if (mGestureDetector.onTouchEvent(e)) {
return true;
}
- final int id = v.getId();
final boolean touchingButton = (id == R.id.close_window || id == R.id.maximize_window
|| id == R.id.open_menu_button);
switch (e.getActionMasked()) {
@@ -674,7 +706,7 @@
mDragPositioningCallback.onDragPositioningStart(
0 /* ctrlType */, e.getRawX(0),
e.getRawY(0));
- mIsDragging = false;
+ updateDragStatus(e.getActionMasked());
mHasLongClicked = false;
// Do not consume input event if a button is touched, otherwise it would
// prevent the button's ripple effect from showing.
@@ -694,7 +726,7 @@
decoration.mTaskSurface,
e.getRawX(dragPointerIdx),
newTaskBounds);
- mIsDragging = true;
+ updateDragStatus(e.getActionMasked());
return true;
}
case MotionEvent.ACTION_UP:
@@ -724,7 +756,7 @@
// onClick call that results.
return false;
} else {
- mIsDragging = false;
+ updateDragStatus(e.getActionMasked());
return true;
}
}
@@ -732,6 +764,21 @@
return true;
}
+ private void updateDragStatus(int eventAction) {
+ switch (eventAction) {
+ case MotionEvent.ACTION_DOWN:
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL: {
+ mIsDragging = false;
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ mIsDragging = true;
+ break;
+ }
+ }
+ }
+
/**
* Perform a task size toggle on release of the double-tap, assuming no drag event
* was handled during the double-tap.
@@ -856,6 +903,10 @@
*
* @param relevantDecor the window decoration of the focused task's caption. This method only
* handles motion events outside this caption's bounds.
+ * TODO(b/349135068): Outside-touch detection no longer works with the
+ * enableAdditionalWindowsAboveStatusBar flag enabled. This
+ * will be fixed once we can add FLAG_WATCH_OUTSIDE_TOUCH to relevant menus,
+ * at which point, all EventReceivers and external touch logic should be removed.
*/
private void handleEventOutsideCaption(MotionEvent ev,
DesktopModeWindowDecoration relevantDecor) {
@@ -908,9 +959,10 @@
dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN
|| windowingMode == WINDOWING_MODE_MULTI_WINDOW;
}
-
- if (dragFromStatusBarAllowed
- && relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)) {
+ final boolean shouldStartTransitionDrag =
+ relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)
+ || Flags.enableAdditionalWindowsAboveStatusBar();
+ if (dragFromStatusBarAllowed && shouldStartTransitionDrag) {
mTransitionDragActive = true;
}
break;
@@ -924,8 +976,15 @@
// Though this isn't a hover event, we need to update handle's hover state
// as it likely will change.
relevantDecor.updateHoverAndPressStatus(ev);
- mDesktopTasksController.onDragPositioningEndThroughStatusBar(
+ DesktopModeVisualIndicator.IndicatorType resultType =
+ mDesktopTasksController.onDragPositioningEndThroughStatusBar(
new PointF(ev.getRawX(), ev.getRawY()), relevantDecor.mTaskInfo);
+ // If we are entering split select, handle will no longer be visible and
+ // should not be receiving any input.
+ if (resultType == TO_SPLIT_LEFT_INDICATOR
+ || resultType != TO_SPLIT_RIGHT_INDICATOR) {
+ relevantDecor.disposeStatusBarInputLayer();
+ }
mMoveToDesktopAnimator = null;
return;
} else {
@@ -937,7 +996,6 @@
relevantDecor.checkTouchEvent(ev);
break;
}
-
case ACTION_MOVE: {
if (relevantDecor == null) {
return;
@@ -1097,6 +1155,7 @@
mDesktopModeWindowDecorFactory.create(
mContext,
mDisplayController,
+ mSplitScreenController,
mTaskOrganizer,
taskInfo,
taskSurface,
@@ -1139,7 +1198,9 @@
windowDecoration.setDragDetector(touchEventListener.mDragDetector);
windowDecoration.relayout(taskInfo, startT, finishT,
false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */);
- incrementEventReceiverTasks(taskInfo.displayId);
+ if (!Flags.enableAdditionalWindowsAboveStatusBar()) {
+ incrementEventReceiverTasks(taskInfo.displayId);
+ }
}
private RunningTaskInfo getOtherSplitTask(int taskId) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 7988983..529def7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -24,6 +24,7 @@
import static android.view.MotionEvent.ACTION_UP;
import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize;
import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize;
@@ -101,6 +102,7 @@
private final @ShellBackgroundThread ShellExecutor mBgExecutor;
private final Choreographer mChoreographer;
private final SyncTransactionQueue mSyncQueue;
+ private final SplitScreenController mSplitScreenController;
private WindowDecorationViewHolder mWindowDecorViewHolder;
private View.OnClickListener mOnCaptionButtonClickListener;
@@ -151,6 +153,7 @@
DesktopModeWindowDecoration(
Context context,
DisplayController displayController,
+ SplitScreenController splitScreenController,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -159,16 +162,18 @@
Choreographer choreographer,
SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
- this (context, displayController, taskOrganizer, taskInfo, taskSurface,
- handler, bgExecutor, choreographer, syncQueue, rootTaskDisplayAreaOrganizer,
- SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
- WindowContainerTransaction::new, SurfaceControl::new,
- new SurfaceControlViewHostFactory() {}, DefaultMaximizeMenuFactory.INSTANCE);
+ this (context, displayController, splitScreenController, taskOrganizer, taskInfo,
+ taskSurface, handler, bgExecutor, choreographer, syncQueue,
+ rootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new,
+ SurfaceControl.Transaction::new, WindowContainerTransaction::new,
+ SurfaceControl::new, new SurfaceControlViewHostFactory() {},
+ DefaultMaximizeMenuFactory.INSTANCE);
}
DesktopModeWindowDecoration(
Context context,
DisplayController displayController,
+ SplitScreenController splitScreenController,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -187,6 +192,7 @@
surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
windowContainerTransactionSupplier, surfaceControlSupplier,
surfaceControlViewHostFactory);
+ mSplitScreenController = splitScreenController;
mHandler = handler;
mBgExecutor = bgExecutor;
mChoreographer = choreographer;
@@ -367,27 +373,41 @@
if (mResult.mRootView == null) {
// This means something blocks the window decor from showing, e.g. the task is hidden.
// Nothing is set up in this case including the decoration surface.
+ disposeStatusBarInputLayer();
Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces
return;
}
if (oldRootView != mResult.mRootView) {
+ disposeStatusBarInputLayer();
mWindowDecorViewHolder = createViewHolder();
}
Trace.beginSection("DesktopModeWindowDecoration#relayout-binding");
- mWindowDecorViewHolder.bindData(mTaskInfo);
+
+ final Point position = new Point();
+ if (isAppHandle(mWindowDecorViewHolder)) {
+ position.set(determineHandlePosition());
+ }
+ mWindowDecorViewHolder.bindData(mTaskInfo,
+ position,
+ mResult.mCaptionWidth,
+ mResult.mCaptionHeight,
+ isCaptionVisible());
Trace.endSection();
if (!mTaskInfo.isFocused) {
closeHandleMenu();
closeMaximizeMenu();
}
-
updateDragResizeListener(oldDecorationSurface);
updateMaximizeMenu(startT);
Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces
}
+ private boolean isCaptionVisible() {
+ return mTaskInfo.isVisible && mIsCaptionVisible;
+ }
+
private void setCapturedLink(Uri capturedLink, long timeStamp) {
if (capturedLink == null
|| (mCapturedLink != null && mCapturedLink.mTimeStamp == timeStamp)) {
@@ -469,12 +489,42 @@
}
}
+ private Point determineHandlePosition() {
+ final Point position = new Point(mResult.mCaptionX, 0);
+ if (mSplitScreenController.getSplitPosition(mTaskInfo.taskId)
+ == SPLIT_POSITION_BOTTOM_OR_RIGHT
+ && mDisplayController.getDisplayLayout(mTaskInfo.displayId).isLandscape()
+ ) {
+ // If this is the right split task, add left stage's width.
+ final Rect leftStageBounds = new Rect();
+ mSplitScreenController.getStageBounds(leftStageBounds, new Rect());
+ position.x += leftStageBounds.width();
+ }
+ return position;
+ }
+
+ /**
+ * Dispose of the view used to forward inputs in status bar region. Intended to be
+ * used any time handle is no longer visible.
+ */
+ void disposeStatusBarInputLayer() {
+ if (!isAppHandle(mWindowDecorViewHolder)
+ || !Flags.enableAdditionalWindowsAboveStatusBar()) {
+ return;
+ }
+ ((AppHandleViewHolder) mWindowDecorViewHolder).disposeStatusBarInputLayer();
+ }
+
private WindowDecorationViewHolder createViewHolder() {
if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) {
return new AppHandleViewHolder(
mResult.mRootView,
mOnCaptionTouchListener,
- mOnCaptionButtonClickListener
+ mOnCaptionButtonClickListener,
+ (v, event) -> {
+ updateHoverAndPressStatus(event);
+ return true;
+ }
);
} else if (mRelayoutParams.mLayoutResId
== R.layout.desktop_mode_app_header) {
@@ -497,6 +547,10 @@
throw new IllegalArgumentException("Unexpected layout resource id");
}
+ private boolean isAppHandle(WindowDecorationViewHolder viewHolder) {
+ return viewHolder instanceof AppHandleViewHolder;
+ }
+
@VisibleForTesting
static void updateRelayoutParams(
RelayoutParams relayoutParams,
@@ -965,10 +1019,17 @@
* @return {@code true} if event is inside caption handle view, {@code false} if not
*/
boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) {
- if (isHandleMenuActive() || !(mWindowDecorViewHolder
- instanceof AppHandleViewHolder)) {
+ if (isHandleMenuActive() || !isAppHandle(mWindowDecorViewHolder)
+ || Flags.enableAdditionalWindowsAboveStatusBar()) {
return false;
}
+ // The status bar input layer can only receive input in handle coordinates to begin with,
+ // so checking coordinates is unnecessary as input is always within handle bounds.
+ if (isAppHandle(mWindowDecorViewHolder)
+ && Flags.enableAdditionalWindowsAboveStatusBar()
+ && isCaptionVisible()) {
+ return true;
+ }
return checkTouchEventInCaption(ev);
}
@@ -1002,7 +1063,7 @@
* @param ev the MotionEvent to compare
*/
void checkTouchEvent(MotionEvent ev) {
- if (mResult.mRootView == null) return;
+ if (mResult.mRootView == null || Flags.enableAdditionalWindowsAboveStatusBar()) return;
final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
final View handle = caption.findViewById(R.id.caption_handle);
final boolean inHandle = !isHandleMenuActive()
@@ -1024,7 +1085,7 @@
* @param ev the MotionEvent to compare against.
*/
void updateHoverAndPressStatus(MotionEvent ev) {
- if (mResult.mRootView == null) return;
+ if (mResult.mRootView == null || Flags.enableAdditionalWindowsAboveStatusBar()) return;
final View handle = mResult.mRootView.findViewById(R.id.caption_handle);
final boolean inHandle = !isHandleMenuActive()
&& checkTouchEventInFocusedCaptionHandle(ev);
@@ -1034,15 +1095,11 @@
// We want handle to remain pressed if the pointer moves outside of it during a drag.
handle.setPressed((inHandle && action == ACTION_DOWN)
|| (handle.isPressed() && action != ACTION_UP && action != ACTION_CANCEL));
- if (isHandleMenuActive() && !isHandleMenuAboveStatusBar()) {
+ if (isHandleMenuActive()) {
mHandleMenu.checkMotionEvent(ev);
}
}
- private boolean isHandleMenuAboveStatusBar() {
- return Flags.enableAdditionalWindowsAboveStatusBar() && !mTaskInfo.isFreeform();
- }
-
private boolean pointInView(View v, float x, float y) {
return v != null && v.getLeft() <= x && v.getRight() >= x
&& v.getTop() <= y && v.getBottom() >= y;
@@ -1054,6 +1111,7 @@
closeHandleMenu();
mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId);
disposeResizeVeil();
+ disposeStatusBarInputLayer();
clearCurrentViewHostRunnable();
super.close();
}
@@ -1153,6 +1211,7 @@
DesktopModeWindowDecoration create(
Context context,
DisplayController displayController,
+ SplitScreenController splitScreenController,
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -1164,6 +1223,7 @@
return new DesktopModeWindowDecoration(
context,
displayController,
+ splitScreenController,
taskOrganizer,
taskInfo,
taskSurface,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index d212f21..a691f59 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -139,7 +139,7 @@
private SurfaceControlViewHost mViewHost;
private Configuration mWindowDecorConfig;
TaskDragResizer mTaskDragResizer;
- private boolean mIsCaptionVisible;
+ boolean mIsCaptionVisible;
/** The most recent set of insets applied to this window decoration. */
private WindowDecorationInsets mWindowDecorationInsets;
@@ -508,6 +508,8 @@
mTaskDragResizer = taskDragResizer;
}
+ // TODO(b/346441962): Move these three methods closer to implementing or View-level classes to
+ // keep implementation details more encapsulated.
private void setCaptionVisibility(View rootView, boolean visible) {
if (rootView == null) {
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
index 03d2421..6a354f1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
@@ -30,18 +30,22 @@
*/
class AdditionalSystemViewContainer(
private val context: Context,
- layoutId: Int,
taskId: Int,
x: Int,
y: Int,
width: Int,
- height: Int
+ height: Int,
+ layoutId: Int? = null
) : AdditionalViewContainer() {
override val view: View
val windowManager: WindowManager? = context.getSystemService(WindowManager::class.java)
init {
- view = LayoutInflater.from(context).inflate(layoutId, null)
+ if (layoutId != null) {
+ view = LayoutInflater.from(context).inflate(layoutId, null)
+ } else {
+ view = View(context)
+ }
val lp = WindowManager.LayoutParams(
width, height, x, y,
WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
index 8d822c2..76dfe37 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
@@ -20,37 +20,68 @@
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.res.ColorStateList
import android.graphics.Color
+import android.graphics.Point
+import android.view.SurfaceControl
import android.view.View
+import android.view.View.OnClickListener
+import android.view.View.OnHoverListener
import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
+import android.view.WindowManager
import android.widget.ImageButton
+import com.android.window.flags.Flags
import com.android.wm.shell.R
import com.android.wm.shell.animation.Interpolators
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
/**
* A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split).
* It hosts a simple handle bar from which to initiate a drag motion to enter desktop mode.
*/
internal class AppHandleViewHolder(
- rootView: View,
- onCaptionTouchListener: View.OnTouchListener,
- onCaptionButtonClickListener: View.OnClickListener
+ rootView: View,
+ private val onCaptionTouchListener: View.OnTouchListener,
+ private val onCaptionButtonClickListener: OnClickListener,
+ private val onCaptionHoverListener: OnHoverListener,
) : WindowDecorationViewHolder(rootView) {
companion object {
private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100
}
-
+ private lateinit var taskInfo: RunningTaskInfo
+ private val windowManager = context.getSystemService(WindowManager::class.java)
private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle)
+ // An invisible View that takes up the same coordinates as captionHandle but is layered
+ // above the status bar. The purpose of this View is to receive input intended for
+ // captionHandle.
+ private var statusBarInputLayer: AdditionalSystemViewContainer? = null
+
init {
captionView.setOnTouchListener(onCaptionTouchListener)
captionHandle.setOnTouchListener(onCaptionTouchListener)
captionHandle.setOnClickListener(onCaptionButtonClickListener)
+ captionHandle.setOnHoverListener(onCaptionHoverListener)
}
- override fun bindData(taskInfo: RunningTaskInfo) {
+ override fun bindData(
+ taskInfo: RunningTaskInfo,
+ position: Point,
+ width: Int,
+ height: Int,
+ isCaptionVisible: Boolean
+ ) {
captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo))
+ this.taskInfo = taskInfo
+ if (!isCaptionVisible && hasStatusBarInputLayer()) {
+ disposeStatusBarInputLayer()
+ return
+ }
+ if (hasStatusBarInputLayer()) {
+ updateStatusBarInputLayer(position)
+ } else {
+ createStatusBarInputLayer(position, width, height)
+ }
}
override fun onHandleMenuOpened() {
@@ -61,6 +92,45 @@
animateCaptionHandleAlpha(startValue = 0f, endValue = 1f)
}
+ private fun createStatusBarInputLayer(handlePosition: Point,
+ handleWidth: Int,
+ handleHeight: Int) {
+ if (!Flags.enableAdditionalWindowsAboveStatusBar()) return
+ statusBarInputLayer = AdditionalSystemViewContainer(context, taskInfo.taskId,
+ handlePosition.x, handlePosition.y, handleWidth, handleHeight)
+ val view = statusBarInputLayer?.view
+ val lp = view?.layoutParams as WindowManager.LayoutParams
+ lp.title = "Handle Input Layer of task " + taskInfo.taskId
+ lp.setTrustedOverlay()
+ // Make this window a spy window to enable it to pilfer pointers from the system-wide
+ // gesture listener that receives events before window. This is to prevent notification
+ // shade gesture when we swipe down to enter desktop.
+ lp.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
+ view.id = R.id.caption_handle
+ view.setOnClickListener(onCaptionButtonClickListener)
+ view.setOnTouchListener(onCaptionTouchListener)
+ view.setOnHoverListener(onCaptionHoverListener)
+ windowManager.updateViewLayout(view, lp)
+ }
+
+ private fun updateStatusBarInputLayer(globalPosition: Point) {
+ statusBarInputLayer?.setPosition(SurfaceControl.Transaction(), globalPosition.x.toFloat(),
+ globalPosition.y.toFloat()) ?: return
+ }
+
+ private fun hasStatusBarInputLayer(): Boolean {
+ return statusBarInputLayer != null
+ }
+
+ /**
+ * Remove the input layer from [WindowManager]. Should be used when caption handle
+ * is not visible.
+ */
+ fun disposeStatusBarInputLayer() {
+ statusBarInputLayer?.releaseView()
+ statusBarInputLayer = null
+ }
+
private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int {
return if (shouldUseLightCaptionColors(taskInfo)) {
context.getColor(R.color.desktop_mode_caption_handle_bar_light)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 46127b1..b704d9c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -21,6 +21,7 @@
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Color
+import android.graphics.Point
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.ShapeDrawable
@@ -136,7 +137,13 @@
onMaximizeHoverAnimationFinishedListener
}
- override fun bindData(taskInfo: RunningTaskInfo) {
+ override fun bindData(
+ taskInfo: RunningTaskInfo,
+ position: Point,
+ width: Int,
+ height: Int,
+ isCaptionVisible: Boolean
+ ) {
if (Flags.enableThemedAppHeaders()) {
bindDataWithThemedHeaders(taskInfo)
} else {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
index 5ae8d25..2341b09 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
@@ -17,6 +17,7 @@
import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
+import android.graphics.Point
import android.view.View
/**
@@ -30,7 +31,13 @@
* A signal to the view holder that new data is available and that the views should be updated to
* reflect it.
*/
- abstract fun bindData(taskInfo: RunningTaskInfo)
+ abstract fun bindData(
+ taskInfo: RunningTaskInfo,
+ position: Point,
+ width: Int,
+ height: Int,
+ isCaptionVisible: Boolean
+ )
/** Callback when the handle menu is opened. */
abstract fun onHandleMenuOpened()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index ae00c3e..aeae0be 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -30,6 +30,7 @@
import android.hardware.display.VirtualDisplay
import android.hardware.input.InputManager
import android.os.Handler
+import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.CheckFlagsRule
@@ -72,6 +73,7 @@
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.sysui.KeyguardChangeListener
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
@@ -79,8 +81,6 @@
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener
-import java.util.Optional
-import java.util.function.Supplier
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
@@ -100,6 +100,8 @@
import org.mockito.kotlin.spy
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
+import java.util.Optional
+import java.util.function.Supplier
/**
* Tests of [DesktopModeWindowDecorViewModel]
@@ -123,6 +125,7 @@
@Mock private lateinit var mockMainChoreographer: Choreographer
@Mock private lateinit var mockTaskOrganizer: ShellTaskOrganizer
@Mock private lateinit var mockDisplayController: DisplayController
+ @Mock private lateinit var mockSplitScreenController: SplitScreenController
@Mock private lateinit var mockDisplayLayout: DisplayLayout
@Mock private lateinit var displayInsetsController: DisplayInsetsController
@Mock private lateinit var mockSyncQueue: SyncTransactionQueue
@@ -174,7 +177,7 @@
mockRootTaskDisplayAreaOrganizer,
windowDecorByTaskIdSpy, mockInteractionJankMonitor
)
-
+ desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController)
whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
whenever(mockDisplayLayout.stableInsets()).thenReturn(STABLE_INSETS)
whenever(mockInputMonitorFactory.create(any(), any())).thenReturn(mockInputMonitor)
@@ -207,6 +210,7 @@
verify(mockDesktopModeWindowDecorFactory).create(
mContext,
mockDisplayController,
+ mockSplitScreenController,
mockTaskOrganizer,
task,
taskSurface,
@@ -232,6 +236,7 @@
verify(mockDesktopModeWindowDecorFactory, never()).create(
mContext,
mockDisplayController,
+ mockSplitScreenController,
mockTaskOrganizer,
task,
taskSurface,
@@ -248,6 +253,7 @@
verify(mockDesktopModeWindowDecorFactory, times(1)).create(
mContext,
mockDisplayController,
+ mockSplitScreenController,
mockTaskOrganizer,
task,
taskSurface,
@@ -260,6 +266,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
fun testCreateAndDisposeEventReceiver() {
val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
setUpMockDecorationForTask(task)
@@ -272,6 +279,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
fun testEventReceiversOnMultipleDisplays() {
val secondaryDisplay = createVirtualDisplay() ?: return
val secondaryDisplayId = secondaryDisplay.display.displayId
@@ -350,7 +358,8 @@
onTaskChanging(task)
verify(mockDesktopModeWindowDecorFactory, never())
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
+ any())
}
@Test
@@ -371,7 +380,8 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory)
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
+ any(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -388,7 +398,8 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory, never())
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
+ any())
}
@Test
@@ -405,7 +416,8 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory, never())
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(),
+ any(), any())
}
@Test
@@ -502,7 +514,8 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory, never())
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
+ any())
} finally {
mockitoSession.finishMocking()
}
@@ -526,7 +539,8 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory)
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
+ any())
} finally {
mockitoSession.finishMocking()
}
@@ -549,7 +563,8 @@
onTaskOpening(task)
verify(mockDesktopModeWindowDecorFactory)
- .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
+ .create(any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(),
+ any())
} finally {
mockitoSession.finishMocking()
}
@@ -688,7 +703,7 @@
val decoration = mock(DesktopModeWindowDecoration::class.java)
whenever(
mockDesktopModeWindowDecorFactory.create(
- any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
+ any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any())
).thenReturn(decoration)
decoration.mTaskInfo = task
whenever(decoration.isFocused).thenReturn(task.isFocused)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index 2c19fdc..412fef3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -87,6 +87,7 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams;
import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener;
@@ -129,6 +130,8 @@
@Mock
private DisplayController mMockDisplayController;
@Mock
+ private SplitScreenController mMockSplitScreenController;
+ @Mock
private ShellTaskOrganizer mMockShellTaskOrganizer;
@Mock
private Choreographer mMockChoreographer;
@@ -395,6 +398,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
public void relayout_fullscreenTask_appliesTransactionImmediately() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
@@ -421,6 +425,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
public void relayout_fullscreenTask_doesNotCreateViewHostImmediately() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
@@ -432,6 +437,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
public void relayout_fullscreenTask_postsViewHostCreation() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
@@ -460,6 +466,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
public void relayout_removesExistingHandlerCallback() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
@@ -474,6 +481,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
public void close_removesExistingHandlerCallback() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
@@ -659,9 +667,9 @@
ActivityManager.RunningTaskInfo taskInfo,
MaximizeMenuFactory maximizeMenuFactory) {
final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
- mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl,
- mMockHandler, mBgExecutor, mMockChoreographer, mMockSyncQueue,
- mMockRootTaskDisplayAreaOrganizer,
+ mMockDisplayController, mMockSplitScreenController, mMockShellTaskOrganizer,
+ taskInfo, mMockSurfaceControl, mMockHandler, mBgExecutor, mMockChoreographer,
+ mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
SurfaceControl.Builder::new, mMockTransactionSupplier,
WindowContainerTransaction::new, SurfaceControl::new,
mMockSurfaceControlViewHostFactory, maximizeMenuFactory);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt
index d3e996b..3b49055 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt
@@ -68,12 +68,12 @@
fun testReleaseView_ViewRemoved() {
viewContainer = AdditionalSystemViewContainer(
mockContext,
- R.layout.desktop_mode_window_decor_handle_menu,
TASK_ID,
X,
Y,
WIDTH,
- HEIGHT
+ HEIGHT,
+ R.layout.desktop_mode_window_decor_handle_menu
)
verify(mockWindowManager).addView(eq(mockView), any())
viewContainer.releaseView()