diff options
4 files changed, 272 insertions, 12 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index eeceaa943af2..45feff5b0ccc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -245,7 +245,10 @@ public abstract class WMShellModule { return new CaptionWindowDecorViewModel( context, mainHandler, + mainExecutor, mainChoreographer, + windowManager, + shellInit, taskOrganizer, displayController, rootTaskDisplayAreaOrganizer, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 95e0d79c212e..b9cb6d3d5007 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -26,12 +26,20 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import android.app.ActivityManager.RunningTaskInfo; import android.content.ContentResolver; import android.content.Context; +import android.graphics.Color; +import android.graphics.Point; import android.graphics.Rect; +import android.graphics.Region; +import android.hardware.input.InputManager; import android.os.Handler; +import android.os.RemoteException; import android.provider.Settings; +import android.util.Log; import android.util.SparseArray; import android.view.Choreographer; import android.view.Display; +import android.view.ISystemGestureExclusionListener; +import android.view.IWindowManager; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; @@ -45,39 +53,74 @@ import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import com.android.wm.shell.windowdecor.extension.TaskInfoKt; /** * View model for the window decoration with a caption and shadows. Works with * {@link CaptionWindowDecoration}. */ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { + private static final String TAG = "CaptionWindowDecorViewModel"; + private final ShellTaskOrganizer mTaskOrganizer; + private final IWindowManager mWindowManager; private final Context mContext; private final Handler mMainHandler; + private final ShellExecutor mMainExecutor; private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final SyncTransactionQueue mSyncQueue; private final Transitions mTransitions; + private final Region mExclusionRegion = Region.obtain(); + private final InputManager mInputManager; private TaskOperations mTaskOperations; + /** + * Whether to pilfer the next motion event to send cancellations to the windows below. + * Useful when the caption window is spy and the gesture should be handled by the system + * instead of by the app for their custom header content. + */ + private boolean mShouldPilferCaptionEvents; + private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); + private final ISystemGestureExclusionListener mGestureExclusionListener = + new ISystemGestureExclusionListener.Stub() { + @Override + public void onSystemGestureExclusionChanged(int displayId, + Region systemGestureExclusion, Region systemGestureExclusionUnrestricted) { + if (mContext.getDisplayId() != displayId) { + return; + } + mMainExecutor.execute(() -> { + mExclusionRegion.set(systemGestureExclusion); + }); + } + }; + public CaptionWindowDecorViewModel( Context context, Handler mainHandler, + ShellExecutor shellExecutor, Choreographer mainChoreographer, + IWindowManager windowManager, + ShellInit shellInit, ShellTaskOrganizer taskOrganizer, DisplayController displayController, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, SyncTransactionQueue syncQueue, Transitions transitions) { mContext = context; + mMainExecutor = shellExecutor; mMainHandler = mainHandler; + mWindowManager = windowManager; mMainChoreographer = mainChoreographer; mTaskOrganizer = taskOrganizer; mDisplayController = displayController; @@ -87,6 +130,18 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { if (!Transitions.ENABLE_SHELL_TRANSITIONS) { mTaskOperations = new TaskOperations(null, mContext, mSyncQueue); } + mInputManager = mContext.getSystemService(InputManager.class); + + shellInit.addInitCallback(this::onInit, this); + } + + private void onInit() { + try { + mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener, + mContext.getDisplayId()); + } catch (RemoteException e) { + Log.e(TAG, "Failed to register window manager callbacks", e); + } } @Override @@ -178,8 +233,12 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) { - final int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); - decoration.setCaptionColor(statusBarColor); + if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { + decoration.setCaptionColor(Color.TRANSPARENT); + } else { + final int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); + decoration.setCaptionColor(statusBarColor); + } } private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { @@ -301,6 +360,49 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue.queue(wct); } } + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + + final int actionMasked = e.getActionMasked(); + final boolean isDown = actionMasked == MotionEvent.ACTION_DOWN; + final boolean isUpOrCancel = actionMasked == MotionEvent.ACTION_CANCEL + || actionMasked == MotionEvent.ACTION_UP; + if (isDown) { + final boolean downInCustomizableCaptionRegion = + decoration.checkTouchEventInCustomizableRegion(e); + final boolean downInExclusionRegion = mExclusionRegion.contains( + (int) e.getRawX(), (int) e.getRawY()); + final boolean isTransparentCaption = + TaskInfoKt.isTransparentCaptionBarAppearance(decoration.mTaskInfo); + // MotionEvent's coordinates are relative to view, we want location in window + // to offset position relative to caption as a whole. + int[] viewLocation = new int[2]; + v.getLocationInWindow(viewLocation); + final boolean isResizeEvent = decoration.shouldResizeListenerHandleEvent(e, + new Point(viewLocation[0], viewLocation[1])); + // The caption window may be a spy window when the caption background is + // transparent, which means events will fall through to the app window. Make + // sure to cancel these events if they do not happen in the intersection of the + // customizable region and what the app reported as exclusion areas, because + // the drag-move or other caption gestures should take priority outside those + // regions. + mShouldPilferCaptionEvents = !(downInCustomizableCaptionRegion + && downInExclusionRegion && isTransparentCaption) && !isResizeEvent; + } + + if (!mShouldPilferCaptionEvents) { + // The event will be handled by a window below or pilfered by resize handler. + return false; + } + // Otherwise pilfer so that windows below receive cancellations for this gesture, and + // continue normal handling as a caption gesture. + if (mInputManager != null) { + // TODO(b/352127475): Only pilfer once per gesture + mInputManager.pilferPointers(v.getViewRootImpl().getInputToken()); + } + if (isUpOrCancel) { + // Gesture is finished, reset state. + mShouldPilferCaptionEvents = false; + } return mDragDetector.onMotionEvent(e); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index d0ca5b0fdce6..7e1b973a98f4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -21,6 +21,7 @@ import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLarge import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import android.annotation.NonNull; +import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; @@ -28,22 +29,27 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.VectorDrawable; import android.os.Handler; import android.util.Size; import android.view.Choreographer; +import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; import android.view.ViewConfiguration; +import android.view.WindowManager; import android.window.WindowContainerTransaction; +import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.windowdecor.extension.TaskInfoKt; /** * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with @@ -177,12 +183,44 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL shouldSetTaskPositionAndCrop); } + @VisibleForTesting + static void updateRelayoutParams( + RelayoutParams relayoutParams, + ActivityManager.RunningTaskInfo taskInfo, + boolean applyStartTransactionOnDraw, + boolean setTaskCropAndPosition) { + relayoutParams.reset(); + relayoutParams.mRunningTaskInfo = taskInfo; + relayoutParams.mLayoutResId = R.layout.caption_window_decor; + relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode()); + relayoutParams.mShadowRadiusId = taskInfo.isFocused + ? R.dimen.freeform_decor_shadow_focused_thickness + : R.dimen.freeform_decor_shadow_unfocused_thickness; + relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; + relayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition; + + if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { + // If the app is requesting to customize the caption bar, allow input to fall + // through to the windows below so that the app can respond to input events on + // their custom content. + relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY; + } + final RelayoutParams.OccludingCaptionElement backButtonElement = + new RelayoutParams.OccludingCaptionElement(); + backButtonElement.mWidthResId = R.dimen.caption_left_buttons_width; + backButtonElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.START; + relayoutParams.mOccludingCaptionElements.add(backButtonElement); + // Then, the right-aligned section (minimize, maximize and close buttons). + final RelayoutParams.OccludingCaptionElement controlsElement = + new RelayoutParams.OccludingCaptionElement(); + controlsElement.mWidthResId = R.dimen.caption_right_buttons_width; + controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END; + relayoutParams.mOccludingCaptionElements.add(controlsElement); + } + void relayout(RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) { - final int shadowRadiusID = taskInfo.isFocused - ? R.dimen.freeform_decor_shadow_focused_thickness - : R.dimen.freeform_decor_shadow_unfocused_thickness; final boolean isFreeform = taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; @@ -191,13 +229,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); - mRelayoutParams.reset(); - mRelayoutParams.mRunningTaskInfo = taskInfo; - mRelayoutParams.mLayoutResId = R.layout.caption_window_decor; - mRelayoutParams.mCaptionHeightId = getCaptionHeightId(taskInfo.getWindowingMode()); - mRelayoutParams.mShadowRadiusId = shadowRadiusID; - mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; - mRelayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition; + updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw, + setTaskCropAndPosition); relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo @@ -303,6 +336,17 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mDragResizeListener = null; } + /** + * Checks whether the touch event falls inside the customizable caption region. + */ + boolean checkTouchEventInCustomizableRegion(MotionEvent ev) { + return mResult.mCustomizableCaptionRegion.contains((int) ev.getRawX(), (int) ev.getRawY()); + } + + boolean shouldResizeListenerHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { + return mDragResizeListener != null && mDragResizeListener.shouldHandleEvent(e, offset); + } + @Override public void close() { closeDragResizeListener(); @@ -311,6 +355,10 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL @Override int getCaptionHeightId(@WindowingMode int windowingMode) { + return getCaptionHeightIdStatic(windowingMode); + } + + private static int getCaptionHeightIdStatic(@WindowingMode int windowingMode) { return R.dimen.freeform_decor_caption_height; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt new file mode 100644 index 000000000000..261d4b5e7d1a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024 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.windowdecor + +import android.app.ActivityManager +import android.app.WindowConfiguration +import android.content.ComponentName +import android.testing.AndroidTestingRunner +import android.view.Display +import android.view.WindowInsetsController +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.google.common.truth.Truth +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class CaptionWindowDecorationTests : ShellTestCase() { + @Test + fun updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() { + val taskInfo = createTaskInfo() + taskInfo.configuration.windowConfiguration.windowingMode = + WindowConfiguration.WINDOWING_MODE_FREEFORM + taskInfo.taskDescription!!.topOpaqueSystemBarsAppearance = + WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND + val relayoutParams = WindowDecoration.RelayoutParams() + + CaptionWindowDecoration.updateRelayoutParams( + relayoutParams, + taskInfo, + true, + false + ) + + Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isTrue() + } + + @Test + fun updateRelayoutParams_freeformButOpaqueAppearance_disallowsInputFallthrough() { + val taskInfo = createTaskInfo() + taskInfo.configuration.windowConfiguration.windowingMode = + WindowConfiguration.WINDOWING_MODE_FREEFORM + taskInfo.taskDescription!!.topOpaqueSystemBarsAppearance = 0 + val relayoutParams = WindowDecoration.RelayoutParams() + + CaptionWindowDecoration.updateRelayoutParams( + relayoutParams, + taskInfo, + true, + false + ) + + Truth.assertThat(relayoutParams.hasInputFeatureSpy()).isFalse() + } + + @Test + fun updateRelayoutParams_addOccludingCaptionElementCorrectly() { + val taskInfo = createTaskInfo() + val relayoutParams = WindowDecoration.RelayoutParams() + CaptionWindowDecoration.updateRelayoutParams( + relayoutParams, + taskInfo, + true, + false + ) + Truth.assertThat(relayoutParams.mOccludingCaptionElements.size).isEqualTo(2) + Truth.assertThat(relayoutParams.mOccludingCaptionElements[0].mAlignment).isEqualTo( + WindowDecoration.RelayoutParams.OccludingCaptionElement.Alignment.START) + Truth.assertThat(relayoutParams.mOccludingCaptionElements[1].mAlignment).isEqualTo( + WindowDecoration.RelayoutParams.OccludingCaptionElement.Alignment.END) + } + + private fun createTaskInfo(): ActivityManager.RunningTaskInfo { + val taskDescriptionBuilder = + ActivityManager.TaskDescription.Builder() + val taskInfo = TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setVisible(true) + .build() + taskInfo.realActivity = ComponentName( + "com.android.wm.shell.windowdecor", + "CaptionWindowDecorationTests" + ) + taskInfo.baseActivity = ComponentName( + "com.android.wm.shell.windowdecor", + "CaptionWindowDecorationTests" + ) + return taskInfo + } +} |