Loading libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +3 −0 Original line number Original line Diff line number Diff line Loading @@ -245,7 +245,10 @@ public abstract class WMShellModule { return new CaptionWindowDecorViewModel( return new CaptionWindowDecorViewModel( context, context, mainHandler, mainHandler, mainExecutor, mainChoreographer, mainChoreographer, windowManager, shellInit, taskOrganizer, taskOrganizer, displayController, displayController, rootTaskDisplayAreaOrganizer, rootTaskDisplayAreaOrganizer, Loading libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +104 −2 Original line number Original line Diff line number Diff line Loading @@ -26,12 +26,20 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityManager.RunningTaskInfo; import android.content.ContentResolver; import android.content.ContentResolver; import android.content.Context; import android.content.Context; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Rect; import android.graphics.Region; import android.hardware.input.InputManager; import android.os.Handler; import android.os.Handler; import android.os.RemoteException; import android.provider.Settings; import android.provider.Settings; import android.util.Log; import android.util.SparseArray; import android.util.SparseArray; import android.view.Choreographer; import android.view.Choreographer; import android.view.Display; import android.view.Display; import android.view.ISystemGestureExclusionListener; import android.view.IWindowManager; import android.view.MotionEvent; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControl; import android.view.View; import android.view.View; Loading @@ -45,39 +53,74 @@ import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; 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.common.SyncTransactionQueue; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.splitscreen.SplitScreenController; 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.transition.Transitions; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; /** /** * View model for the window decoration with a caption and shadows. Works with * View model for the window decoration with a caption and shadows. Works with * {@link CaptionWindowDecoration}. * {@link CaptionWindowDecoration}. */ */ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private static final String TAG = "CaptionWindowDecorViewModel"; private final ShellTaskOrganizer mTaskOrganizer; private final ShellTaskOrganizer mTaskOrganizer; private final IWindowManager mWindowManager; private final Context mContext; private final Context mContext; private final Handler mMainHandler; private final Handler mMainHandler; private final ShellExecutor mMainExecutor; private final Choreographer mMainChoreographer; private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; private final DisplayController mDisplayController; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final SyncTransactionQueue mSyncQueue; private final SyncTransactionQueue mSyncQueue; private final Transitions mTransitions; private final Transitions mTransitions; private final Region mExclusionRegion = Region.obtain(); private final InputManager mInputManager; private TaskOperations mTaskOperations; 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 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( public CaptionWindowDecorViewModel( Context context, Context context, Handler mainHandler, Handler mainHandler, ShellExecutor shellExecutor, Choreographer mainChoreographer, Choreographer mainChoreographer, IWindowManager windowManager, ShellInit shellInit, ShellTaskOrganizer taskOrganizer, ShellTaskOrganizer taskOrganizer, DisplayController displayController, DisplayController displayController, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, SyncTransactionQueue syncQueue, SyncTransactionQueue syncQueue, Transitions transitions) { Transitions transitions) { mContext = context; mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; mMainHandler = mainHandler; mWindowManager = windowManager; mMainChoreographer = mainChoreographer; mMainChoreographer = mainChoreographer; mTaskOrganizer = taskOrganizer; mTaskOrganizer = taskOrganizer; mDisplayController = displayController; mDisplayController = displayController; Loading @@ -87,6 +130,18 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { if (!Transitions.ENABLE_SHELL_TRANSITIONS) { if (!Transitions.ENABLE_SHELL_TRANSITIONS) { mTaskOperations = new TaskOperations(null, mContext, mSyncQueue); 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 @Override Loading Loading @@ -178,9 +233,13 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } } private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) { private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) { if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { decoration.setCaptionColor(Color.TRANSPARENT); } else { final int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); final int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); decoration.setCaptionColor(statusBarColor); decoration.setCaptionColor(statusBarColor); } } } private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { Loading Loading @@ -301,6 +360,49 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue.queue(wct); 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); return mDragDetector.onMotionEvent(e); } } Loading libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +58 −10 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLarge import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import android.annotation.NonNull; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityManager.RunningTaskInfo; import android.app.WindowConfiguration; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; import android.app.WindowConfiguration.WindowingMode; Loading @@ -28,22 +29,27 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.VectorDrawable; import android.graphics.drawable.VectorDrawable; import android.os.Handler; import android.os.Handler; import android.util.Size; import android.util.Size; import android.view.Choreographer; import android.view.Choreographer; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControl; import android.view.View; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewConfiguration; import android.view.WindowManager; import android.window.WindowContainerTransaction; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; 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 * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with Loading Loading @@ -177,12 +183,44 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL shouldSetTaskPositionAndCrop); 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, void relayout(RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) { 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 = final boolean isFreeform = taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; Loading @@ -191,13 +229,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); final WindowContainerTransaction wct = new WindowContainerTransaction(); mRelayoutParams.reset(); updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw, mRelayoutParams.mRunningTaskInfo = taskInfo; setTaskCropAndPosition); mRelayoutParams.mLayoutResId = R.layout.caption_window_decor; mRelayoutParams.mCaptionHeightId = getCaptionHeightId(taskInfo.getWindowingMode()); mRelayoutParams.mShadowRadiusId = shadowRadiusID; mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; mRelayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition; relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo Loading Loading @@ -303,6 +336,17 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mDragResizeListener = null; 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 @Override public void close() { public void close() { closeDragResizeListener(); closeDragResizeListener(); Loading @@ -311,6 +355,10 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL @Override @Override int getCaptionHeightId(@WindowingMode int windowingMode) { int getCaptionHeightId(@WindowingMode int windowingMode) { return getCaptionHeightIdStatic(windowingMode); } private static int getCaptionHeightIdStatic(@WindowingMode int windowingMode) { return R.dimen.freeform_decor_caption_height; return R.dimen.freeform_decor_caption_height; } } Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt 0 → 100644 +107 −0 Original line number Original line Diff line number Diff line /* * 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 } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +3 −0 Original line number Original line Diff line number Diff line Loading @@ -245,7 +245,10 @@ public abstract class WMShellModule { return new CaptionWindowDecorViewModel( return new CaptionWindowDecorViewModel( context, context, mainHandler, mainHandler, mainExecutor, mainChoreographer, mainChoreographer, windowManager, shellInit, taskOrganizer, taskOrganizer, displayController, displayController, rootTaskDisplayAreaOrganizer, rootTaskDisplayAreaOrganizer, Loading
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +104 −2 Original line number Original line Diff line number Diff line Loading @@ -26,12 +26,20 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityManager.RunningTaskInfo; import android.content.ContentResolver; import android.content.ContentResolver; import android.content.Context; import android.content.Context; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Rect; import android.graphics.Region; import android.hardware.input.InputManager; import android.os.Handler; import android.os.Handler; import android.os.RemoteException; import android.provider.Settings; import android.provider.Settings; import android.util.Log; import android.util.SparseArray; import android.util.SparseArray; import android.view.Choreographer; import android.view.Choreographer; import android.view.Display; import android.view.Display; import android.view.ISystemGestureExclusionListener; import android.view.IWindowManager; import android.view.MotionEvent; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControl; import android.view.View; import android.view.View; Loading @@ -45,39 +53,74 @@ import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; 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.common.SyncTransactionQueue; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.splitscreen.SplitScreenController; 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.transition.Transitions; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; /** /** * View model for the window decoration with a caption and shadows. Works with * View model for the window decoration with a caption and shadows. Works with * {@link CaptionWindowDecoration}. * {@link CaptionWindowDecoration}. */ */ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private static final String TAG = "CaptionWindowDecorViewModel"; private final ShellTaskOrganizer mTaskOrganizer; private final ShellTaskOrganizer mTaskOrganizer; private final IWindowManager mWindowManager; private final Context mContext; private final Context mContext; private final Handler mMainHandler; private final Handler mMainHandler; private final ShellExecutor mMainExecutor; private final Choreographer mMainChoreographer; private final Choreographer mMainChoreographer; private final DisplayController mDisplayController; private final DisplayController mDisplayController; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private final SyncTransactionQueue mSyncQueue; private final SyncTransactionQueue mSyncQueue; private final Transitions mTransitions; private final Transitions mTransitions; private final Region mExclusionRegion = Region.obtain(); private final InputManager mInputManager; private TaskOperations mTaskOperations; 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 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( public CaptionWindowDecorViewModel( Context context, Context context, Handler mainHandler, Handler mainHandler, ShellExecutor shellExecutor, Choreographer mainChoreographer, Choreographer mainChoreographer, IWindowManager windowManager, ShellInit shellInit, ShellTaskOrganizer taskOrganizer, ShellTaskOrganizer taskOrganizer, DisplayController displayController, DisplayController displayController, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, SyncTransactionQueue syncQueue, SyncTransactionQueue syncQueue, Transitions transitions) { Transitions transitions) { mContext = context; mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; mMainHandler = mainHandler; mWindowManager = windowManager; mMainChoreographer = mainChoreographer; mMainChoreographer = mainChoreographer; mTaskOrganizer = taskOrganizer; mTaskOrganizer = taskOrganizer; mDisplayController = displayController; mDisplayController = displayController; Loading @@ -87,6 +130,18 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { if (!Transitions.ENABLE_SHELL_TRANSITIONS) { if (!Transitions.ENABLE_SHELL_TRANSITIONS) { mTaskOperations = new TaskOperations(null, mContext, mSyncQueue); 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 @Override Loading Loading @@ -178,9 +233,13 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } } private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) { private void setupCaptionColor(RunningTaskInfo taskInfo, CaptionWindowDecoration decoration) { if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) { decoration.setCaptionColor(Color.TRANSPARENT); } else { final int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); final int statusBarColor = taskInfo.taskDescription.getStatusBarColor(); decoration.setCaptionColor(statusBarColor); decoration.setCaptionColor(statusBarColor); } } } private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { Loading Loading @@ -301,6 +360,49 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mSyncQueue.queue(wct); 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); return mDragDetector.onMotionEvent(e); } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +58 −10 Original line number Original line Diff line number Diff line Loading @@ -21,6 +21,7 @@ import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLarge import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import android.annotation.NonNull; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityManager.RunningTaskInfo; import android.app.WindowConfiguration; import android.app.WindowConfiguration; import android.app.WindowConfiguration.WindowingMode; import android.app.WindowConfiguration.WindowingMode; Loading @@ -28,22 +29,27 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Color; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.VectorDrawable; import android.graphics.drawable.VectorDrawable; import android.os.Handler; import android.os.Handler; import android.util.Size; import android.util.Size; import android.view.Choreographer; import android.view.Choreographer; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControl; import android.view.View; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewConfiguration; import android.view.WindowManager; import android.window.WindowContainerTransaction; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; 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 * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with Loading Loading @@ -177,12 +183,44 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL shouldSetTaskPositionAndCrop); 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, void relayout(RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition) { 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 = final boolean isFreeform = taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; final boolean isDragResizeable = isFreeform && taskInfo.isResizeable; Loading @@ -191,13 +229,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; final WindowContainerTransaction wct = new WindowContainerTransaction(); final WindowContainerTransaction wct = new WindowContainerTransaction(); mRelayoutParams.reset(); updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw, mRelayoutParams.mRunningTaskInfo = taskInfo; setTaskCropAndPosition); mRelayoutParams.mLayoutResId = R.layout.caption_window_decor; mRelayoutParams.mCaptionHeightId = getCaptionHeightId(taskInfo.getWindowingMode()); mRelayoutParams.mShadowRadiusId = shadowRadiusID; mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; mRelayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition; relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo // After this line, mTaskInfo is up-to-date and should be used instead of taskInfo Loading Loading @@ -303,6 +336,17 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mDragResizeListener = null; 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 @Override public void close() { public void close() { closeDragResizeListener(); closeDragResizeListener(); Loading @@ -311,6 +355,10 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL @Override @Override int getCaptionHeightId(@WindowingMode int windowingMode) { int getCaptionHeightId(@WindowingMode int windowingMode) { return getCaptionHeightIdStatic(windowingMode); } private static int getCaptionHeightIdStatic(@WindowingMode int windowingMode) { return R.dimen.freeform_decor_caption_height; return R.dimen.freeform_decor_caption_height; } } Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt 0 → 100644 +107 −0 Original line number Original line Diff line number Diff line /* * 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 } }