diff options
Diffstat (limited to 'libs')
7 files changed, 607 insertions, 262 deletions
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml index 7d5f9cdbebc8..5fe3f2af63a0 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -14,88 +14,100 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/maximize_menu" - style="?android:attr/buttonBarStyle" android:layout_width="@dimen/desktop_mode_maximize_menu_width" android:layout_height="@dimen/desktop_mode_maximize_menu_height" - android:orientation="horizontal" - android:gravity="center" - android:padding="16dp" android:background="@drawable/desktop_mode_maximize_menu_background" android:elevation="1dp"> <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical"> + android:id="@+id/container" + android:layout_width="@dimen/desktop_mode_maximize_menu_width" + android:layout_height="@dimen/desktop_mode_maximize_menu_height" + android:orientation="horizontal" + android:padding="16dp" + android:gravity="center"> - <Button - android:layout_width="94dp" - android:layout_height="60dp" - android:id="@+id/maximize_menu_maximize_button" - style="?android:attr/buttonBarButtonStyle" - android:stateListAnimator="@null" - android:layout_marginRight="8dp" - android:layout_marginBottom="4dp" - android:alpha="0"/> - - <TextView - android:id="@+id/maximize_menu_maximize_window_text" - android:layout_width="94dp" - android:layout_height="18dp" - android:textSize="11sp" - android:layout_marginBottom="76dp" - android:gravity="center" - android:fontFamily="google-sans-text" - android:text="@string/desktop_mode_maximize_menu_maximize_text" - android:textColor="?androidprv:attr/materialColorOnSurface" - android:alpha="0"/> - </LinearLayout> - - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical"> <LinearLayout - android:id="@+id/maximize_menu_snap_menu_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="horizontal" - android:padding="4dp" - android:background="@drawable/desktop_mode_maximize_menu_layout_background" - android:layout_marginBottom="4dp" - android:alpha="0"> - <Button - android:id="@+id/maximize_menu_snap_left_button" - style="?android:attr/buttonBarButtonStyle" - android:layout_width="41dp" - android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" - android:layout_marginRight="4dp" - android:background="@drawable/desktop_mode_maximize_menu_button_background" - android:stateListAnimator="@null"/> + android:orientation="vertical"> <Button - android:id="@+id/maximize_menu_snap_right_button" + android:layout_width="94dp" + android:layout_height="60dp" + android:id="@+id/maximize_menu_maximize_button" style="?android:attr/buttonBarButtonStyle" - android:layout_width="41dp" - android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" - android:background="@drawable/desktop_mode_maximize_menu_button_background" - android:stateListAnimator="@null"/> + android:stateListAnimator="@null" + android:layout_marginRight="8dp" + android:layout_marginBottom="4dp" + android:alpha="0"/> + + <TextView + android:id="@+id/maximize_menu_maximize_window_text" + android:layout_width="94dp" + android:layout_height="18dp" + android:textSize="11sp" + android:layout_marginBottom="76dp" + android:gravity="center" + android:fontFamily="google-sans-text" + android:text="@string/desktop_mode_maximize_menu_maximize_text" + android:textColor="?androidprv:attr/materialColorOnSurface" + android:alpha="0"/> + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + <LinearLayout + android:id="@+id/maximize_menu_snap_menu_layout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="4dp" + android:background="@drawable/desktop_mode_maximize_menu_layout_background" + android:layout_marginBottom="4dp" + android:alpha="0"> + <Button + android:id="@+id/maximize_menu_snap_left_button" + style="?android:attr/buttonBarButtonStyle" + android:layout_width="41dp" + android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" + android:layout_marginRight="4dp" + android:background="@drawable/desktop_mode_maximize_menu_button_background" + android:stateListAnimator="@null"/> + + <Button + android:id="@+id/maximize_menu_snap_right_button" + style="?android:attr/buttonBarButtonStyle" + android:layout_width="41dp" + android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" + android:background="@drawable/desktop_mode_maximize_menu_button_background" + android:stateListAnimator="@null"/> + </LinearLayout> + <TextView + android:id="@+id/maximize_menu_snap_window_text" + android:layout_width="94dp" + android:layout_height="18dp" + android:textSize="11sp" + android:layout_marginBottom="76dp" + android:layout_gravity="center" + android:gravity="center" + android:fontFamily="google-sans-text" + android:text="@string/desktop_mode_maximize_menu_snap_text" + android:textColor="?androidprv:attr/materialColorOnSurface" + android:alpha="0"/> </LinearLayout> - <TextView - android:id="@+id/maximize_menu_snap_window_text" - android:layout_width="94dp" - android:layout_height="18dp" - android:textSize="11sp" - android:layout_marginBottom="76dp" - android:layout_gravity="center" - android:gravity="center" - android:fontFamily="google-sans-text" - android:text="@string/desktop_mode_maximize_menu_snap_text" - android:textColor="?androidprv:attr/materialColorOnSurface" - android:alpha="0"/> </LinearLayout> -</LinearLayout> + + <!-- Empty view intentionally placed in front of everything else and matching the menu size + used to monitor input events over the entire menu. --> + <View + android:id="@+id/maximize_menu_overlay" + android:layout_width="@dimen/desktop_mode_maximize_menu_width" + android:layout_height="@dimen/desktop_mode_maximize_menu_height"/> +</FrameLayout> 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 e1009a0ae8bb..180e4f999726 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 @@ -26,7 +26,6 @@ import static android.view.InputDevice.SOURCE_TOUCHSCREEN; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_HOVER_ENTER; import static android.view.MotionEvent.ACTION_HOVER_EXIT; -import static android.view.MotionEvent.ACTION_HOVER_MOVE; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; import static android.view.WindowInsets.Type.statusBars; @@ -103,6 +102,7 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; +import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import java.io.PrintWriter; import java.util.Objects; @@ -383,10 +383,32 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mWindowDecorByTaskId.remove(taskInfo.taskId); } + private void onMaximizeOrRestore(int taskId, String tag) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) { + return; + } + InteractionJankMonitorUtils.beginTracing( + Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, mContext, decoration.mTaskSurface, tag); + mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo); + decoration.closeHandleMenu(); + decoration.closeMaximizeMenu(); + } + + private void onSnapResize(int taskId, boolean left) { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); + if (decoration == null) { + return; + } + mDesktopTasksController.snapToHalfScreen(decoration.mTaskInfo, + left ? SnapPosition.LEFT : SnapPosition.RIGHT); + decoration.closeHandleMenu(); + decoration.closeMaximizeMenu(); + } + private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, View.OnGenericMotionListener, DragDetector.MotionEventHandler { - private static final int CLOSE_MAXIMIZE_MENU_DELAY_MS = 150; private final int mTaskId; private final WindowContainerToken mTaskToken; @@ -405,7 +427,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private boolean mTouchscreenInUse; private boolean mHasLongClicked; private int mDragPointerId = -1; - private final Runnable mCloseMaximizeWindowRunnable; private DesktopModeTouchEventListener( RunningTaskInfo taskInfo, @@ -416,11 +437,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDragDetector = new DragDetector(this); mGestureDetector = new GestureDetector(mContext, this); mDisplayId = taskInfo.displayId; - mCloseMaximizeWindowRunnable = () -> { - final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); - if (decoration == null) return; - decoration.closeMaximizeMenu(); - }; } @Override @@ -472,31 +488,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (id == R.id.collapse_menu_button) { decoration.closeHandleMenu(); } else if (id == R.id.maximize_window) { - InteractionJankMonitorUtils.beginTracing( - Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v, - /* tag= */ "caption_bar_button"); - final RunningTaskInfo taskInfo = decoration.mTaskInfo; - decoration.closeHandleMenu(); - decoration.closeMaximizeMenu(); - mDesktopTasksController.toggleDesktopTaskSize(taskInfo); - } else if (id == R.id.maximize_menu_maximize_button) { - InteractionJankMonitorUtils.beginTracing( - Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v, - /* tag= */ "maximize_menu_option"); - final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.toggleDesktopTaskSize(taskInfo); - decoration.closeHandleMenu(); - decoration.closeMaximizeMenu(); - } else if (id == R.id.maximize_menu_snap_left_button) { - final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.LEFT); - decoration.closeHandleMenu(); - decoration.closeMaximizeMenu(); - } else if (id == R.id.maximize_menu_snap_right_button) { - final RunningTaskInfo taskInfo = decoration.mTaskInfo; - mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.RIGHT); - decoration.closeHandleMenu(); - decoration.closeMaximizeMenu(); + // TODO(b/346441962): move click detection logic into the decor's + // {@link AppHeaderViewHolder}. Let it encapsulate the that and have it report + // back to the decoration using + // {@link DesktopModeWindowDecoration#setOnMaximizeOrRestoreClickListener}, which + // should shared with the maximize menu's maximize/restore actions. + onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button"); } } @@ -578,40 +575,26 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return false; } + /** + * TODO(b/346441962): move this hover detection logic into the decor's + * {@link AppHeaderViewHolder}. + */ @Override public boolean onGenericMotion(View v, MotionEvent ev) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final int id = v.getId(); - if (ev.getAction() == ACTION_HOVER_ENTER) { - if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) { - decoration.onMaximizeWindowHoverEnter(); - } else if (id == R.id.maximize_window - || MaximizeMenu.Companion.isMaximizeMenuView(id)) { - // Re-hovering over any of the maximize menu views should keep the menu open by - // cancelling any attempts to close the menu. - mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable); - if (id != R.id.maximize_window) { - decoration.onMaximizeMenuHoverEnter(id, ev); - } + if (ev.getAction() == ACTION_HOVER_ENTER && id == R.id.maximize_window) { + decoration.setAppHeaderMaximizeButtonHovered(true); + if (!decoration.isMaximizeMenuActive()) { + decoration.onMaximizeButtonHoverEnter(); } return true; - } else if (ev.getAction() == ACTION_HOVER_MOVE - && MaximizeMenu.Companion.isMaximizeMenuView(id)) { - decoration.onMaximizeMenuHoverMove(id, ev); - mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable); - } else if (ev.getAction() == ACTION_HOVER_EXIT) { - if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) { - decoration.onMaximizeWindowHoverExit(); - } else if (id == R.id.maximize_window - || MaximizeMenu.Companion.isMaximizeMenuView(id)) { - // Close menu if not hovering over maximize menu or maximize button after a - // delay to give user a chance to re-enter view or to move from one maximize - // menu view to another. - mMainHandler.postDelayed(mCloseMaximizeWindowRunnable, - CLOSE_MAXIMIZE_MENU_DELAY_MS); - if (id != R.id.maximize_window) { - decoration.onMaximizeMenuHoverExit(id, ev); - } + } + if (ev.getAction() == ACTION_HOVER_EXIT && id == R.id.maximize_window) { + decoration.setAppHeaderMaximizeButtonHovered(false); + decoration.onMaximizeHoverStateChanged(); + if (!decoration.isMaximizeMenuActive()) { + decoration.onMaximizeButtonHoverExit(); } return true; } @@ -719,11 +702,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && action != MotionEvent.ACTION_CANCEL)) { return false; } - final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); - InteractionJankMonitorUtils.beginTracing( - Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, mContext, - /* surface= */ decoration.mTaskSurface, /* tag= */ "double_tap"); - mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo); + onMaximizeOrRestore(mTaskId, "double_tap"); return true; } } @@ -1105,7 +1084,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { final DesktopModeTouchEventListener touchEventListener = new DesktopModeTouchEventListener(taskInfo, dragPositioningCallback); - + windowDecoration.setOnMaximizeOrRestoreClickListener(this::onMaximizeOrRestore); + windowDecoration.setOnLeftSnapClickListener((taskId, tag) -> { + onSnapResize(taskId, true /* isLeft */); + }); + windowDecoration.setOnRightSnapClickListener((taskId, tag) -> { + onSnapResize(taskId, false /* isLeft */); + }); windowDecoration.setCaptionListeners( touchEventListener, touchEventListener, touchEventListener, touchEventListener); windowDecoration.setExclusionRegionListener(mExclusionRegionListener); 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 4d597cac889e..f53c21d352b3 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 @@ -69,6 +69,7 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder; import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; @@ -87,6 +88,9 @@ import java.util.function.Supplier; public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { private static final String TAG = "DesktopModeWindowDecoration"; + @VisibleForTesting + static final long CLOSE_MAXIMIZE_MENU_DELAY_MS = 150L; + private final Handler mHandler; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; @@ -96,6 +100,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private View.OnTouchListener mOnCaptionTouchListener; private View.OnLongClickListener mOnCaptionLongClickListener; private View.OnGenericMotionListener mOnCaptionGenericMotionListener; + private OnTaskActionClickListener mOnMaximizeOrRestoreClickListener; + private OnTaskActionClickListener mOnLeftSnapClickListener; + private OnTaskActionClickListener mOnRightSnapClickListener; private DragPositioningCallback mDragPositioningCallback; private DragResizeInputListener mDragResizeListener; private DragDetector mDragDetector; @@ -120,6 +127,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private ExclusionRegionListener mExclusionRegionListener; private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + private final MaximizeMenuFactory mMaximizeMenuFactory; + + // Hover state for the maximize menu and button. The menu will remain open as long as either of + // these is true. See {@link #onMaximizeHoverStateChanged()}. + private boolean mIsAppHeaderMaximizeButtonHovered = false; + private boolean mIsMaximizeMenuHovered = false; + // Used to schedule the closing of the maximize menu when neither of the button or menu are + // being hovered. There's a small delay after stopping the hover, to allow a quick reentry + // to cancel the close. + private final Runnable mCloseMaximizeWindowRunnable = this::closeMaximizeMenu; DesktopModeWindowDecoration( Context context, @@ -135,7 +152,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new, SurfaceControl.Transaction::new, WindowContainerTransaction::new, SurfaceControl::new, - new SurfaceControlViewHostFactory() {}); + new SurfaceControlViewHostFactory() {}, + DefaultMaximizeMenuFactory.INSTANCE); } DesktopModeWindowDecoration( @@ -152,7 +170,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, Supplier<WindowContainerTransaction> windowContainerTransactionSupplier, Supplier<SurfaceControl> surfaceControlSupplier, - SurfaceControlViewHostFactory surfaceControlViewHostFactory) { + SurfaceControlViewHostFactory surfaceControlViewHostFactory, + MaximizeMenuFactory maximizeMenuFactory) { super(context, displayController, taskOrganizer, taskInfo, taskSurface, surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, windowContainerTransactionSupplier, surfaceControlSupplier, @@ -161,6 +180,31 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mChoreographer = choreographer; mSyncQueue = syncQueue; mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + mMaximizeMenuFactory = maximizeMenuFactory; + } + + /** + * Register a listener to be called back when one of the tasks' maximize/restore action is + * triggered. + * TODO(b/346441962): hook this up to double-tap and the header's maximize button, instead of + * having the ViewModel deal with parsing motion events. + */ + void setOnMaximizeOrRestoreClickListener(OnTaskActionClickListener listener) { + mOnMaximizeOrRestoreClickListener = listener; + } + + /** + * Register a listener to be called back when one of the tasks snap-left action is triggered. + */ + void setOnLeftSnapClickListener(OnTaskActionClickListener listener) { + mOnLeftSnapClickListener = listener; + } + + /** + * Register a listener to be called back when one of the tasks' snap-right action is triggered. + */ + void setOnRightSnapClickListener(OnTaskActionClickListener listener) { + mOnRightSnapClickListener = listener; } void setCaptionListeners( @@ -714,11 +758,41 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Create and display maximize menu window */ void createMaximizeMenu() { - mMaximizeMenu = new MaximizeMenu(mSyncQueue, mRootTaskDisplayAreaOrganizer, - mDisplayController, mTaskInfo, mOnCaptionButtonClickListener, - mOnCaptionGenericMotionListener, mOnCaptionTouchListener, mContext, + mMaximizeMenu = mMaximizeMenuFactory.create(mSyncQueue, mRootTaskDisplayAreaOrganizer, + mDisplayController, mTaskInfo, mContext, calculateMaximizeMenuPosition(), mSurfaceControlTransactionSupplier); - mMaximizeMenu.show(); + mMaximizeMenu.show( + mOnMaximizeOrRestoreClickListener, + mOnLeftSnapClickListener, + mOnRightSnapClickListener, + hovered -> { + mIsMaximizeMenuHovered = hovered; + onMaximizeHoverStateChanged(); + return null; + } + ); + } + + /** Set whether the app header's maximize button is hovered. */ + void setAppHeaderMaximizeButtonHovered(boolean hovered) { + mIsAppHeaderMaximizeButtonHovered = hovered; + onMaximizeHoverStateChanged(); + } + + /** + * Called when either one of the maximize button in the app header or the maximize menu has + * changed its hover state. + */ + void onMaximizeHoverStateChanged() { + if (!mIsMaximizeMenuHovered && !mIsAppHeaderMaximizeButtonHovered) { + // Neither is hovered, close the menu. + if (isMaximizeMenuActive()) { + mHandler.postDelayed(mCloseMaximizeWindowRunnable, CLOSE_MAXIMIZE_MENU_DELAY_MS); + } + return; + } + // At least one of the two is hovered, cancel the close if needed. + mHandler.removeCallbacks(mCloseMaximizeWindowRunnable); } /** @@ -992,34 +1066,22 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin .setAnimatingTaskResize(animatingTaskResize); } - /** Called when there is a {@Link ACTION_HOVER_EXIT} on the maximize window button. */ - void onMaximizeWindowHoverExit() { + /** + * Called when there is a {@link MotionEvent#ACTION_HOVER_EXIT} on the maximize window button. + */ + void onMaximizeButtonHoverExit() { ((AppHeaderViewHolder) mWindowDecorViewHolder) .onMaximizeWindowHoverExit(); } - /** Called when there is a {@Link ACTION_HOVER_ENTER} on the maximize window button. */ - void onMaximizeWindowHoverEnter() { + /** + * Called when there is a {@link MotionEvent#ACTION_HOVER_ENTER} on the maximize window button. + */ + void onMaximizeButtonHoverEnter() { ((AppHeaderViewHolder) mWindowDecorViewHolder) .onMaximizeWindowHoverEnter(); } - /** Called when there is a {@Link ACTION_HOVER_ENTER} on a view in the maximize menu. */ - void onMaximizeMenuHoverEnter(int id, MotionEvent ev) { - mMaximizeMenu.onMaximizeMenuHoverEnter(id, ev); - } - - /** Called when there is a {@Link ACTION_HOVER_MOVE} on a view in the maximize menu. */ - void onMaximizeMenuHoverMove(int id, MotionEvent ev) { - mMaximizeMenu.onMaximizeMenuHoverMove(id, ev); - } - - /** Called when there is a {@Link ACTION_HOVER_EXIT} on a view in the maximize menu. */ - void onMaximizeMenuHoverExit(int id, MotionEvent ev) { - mMaximizeMenu.onMaximizeMenuHoverExit(id, ev); - } - - @Override public String toString() { return "{" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 0470367015ea..5f9f8d6d1764 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -20,7 +20,6 @@ import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.annotation.ColorInt -import android.annotation.IdRes import android.app.ActivityManager.RunningTaskInfo import android.content.Context import android.content.res.ColorStateList @@ -28,6 +27,7 @@ import android.content.res.Resources import android.graphics.Paint import android.graphics.PixelFormat import android.graphics.PointF +import android.graphics.Rect import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.graphics.drawable.LayerDrawable @@ -37,16 +37,17 @@ import android.graphics.drawable.shapes.RoundRectShape import android.util.StateSet import android.view.LayoutInflater import android.view.MotionEvent +import android.view.MotionEvent.ACTION_HOVER_ENTER +import android.view.MotionEvent.ACTION_HOVER_EXIT +import android.view.MotionEvent.ACTION_HOVER_MOVE import android.view.SurfaceControl import android.view.SurfaceControl.Transaction import android.view.SurfaceControlViewHost import android.view.View -import android.view.View.OnClickListener -import android.view.View.OnGenericMotionListener -import android.view.View.OnTouchListener import android.view.View.SCALE_Y import android.view.View.TRANSLATION_Y import android.view.View.TRANSLATION_Z +import android.view.ViewGroup import android.view.WindowManager import android.view.WindowlessWindowManager import android.widget.Button @@ -64,10 +65,10 @@ import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHo import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.OPACITY_12 import com.android.wm.shell.windowdecor.common.OPACITY_40 +import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener import com.android.wm.shell.windowdecor.common.withAlpha import java.util.function.Supplier - /** * Menu that appears when user long clicks the maximize button. Gives the user the option to * maximize the task or snap the task to the right or left half of the screen. @@ -77,9 +78,6 @@ class MaximizeMenu( private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer, private val displayController: DisplayController, private val taskInfo: RunningTaskInfo, - private val onClickListener: OnClickListener, - private val onGenericMotionListener: OnGenericMotionListener, - private val onTouchListener: OnTouchListener, private val decorWindowContext: Context, private val menuPosition: PointF, private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() } @@ -102,9 +100,19 @@ class MaximizeMenu( } /** Creates and shows the maximize window. */ - fun show() { + fun show( + onMaximizeClickListener: OnTaskActionClickListener, + onLeftSnapClickListener: OnTaskActionClickListener, + onRightSnapClickListener: OnTaskActionClickListener, + onHoverListener: (Boolean) -> Unit + ) { if (maximizeMenu != null) return - createMaximizeMenu() + createMaximizeMenu( + onMaximizeClickListener = onMaximizeClickListener, + onLeftSnapClickListener = onLeftSnapClickListener, + onRightSnapClickListener = onRightSnapClickListener, + onHoverListener = onHoverListener + ) maximizeMenuView?.animateOpenMenu() } @@ -117,7 +125,12 @@ class MaximizeMenu( } /** Create a maximize menu that is attached to the display area. */ - private fun createMaximizeMenu() { + private fun createMaximizeMenu( + onMaximizeClickListener: OnTaskActionClickListener, + onLeftSnapClickListener: OnTaskActionClickListener, + onRightSnapClickListener: OnTaskActionClickListener, + onHoverListener: (Boolean) -> Unit + ) { val t = transactionSupplier.get() val builder = SurfaceControl.Builder() rootTdaOrganizer.attachToDisplayArea(taskInfo.displayId, builder) @@ -146,11 +159,19 @@ class MaximizeMenu( context = decorWindowContext, menuHeight = menuHeight, menuPadding = menuPadding, - onClickListener = onClickListener, - onTouchListener = onTouchListener, - onGenericMotionListener = onGenericMotionListener, ).also { menuView -> + val taskId = taskInfo.taskId menuView.bind(taskInfo) + menuView.onMaximizeClickListener = { + onMaximizeClickListener.onClick(taskId, "maximize_menu_option") + } + menuView.onLeftSnapClickListener = { + onLeftSnapClickListener.onClick(taskId, "left_snap_option") + } + menuView.onRightSnapClickListener = { + onRightSnapClickListener.onClick(taskId, "right_snap_option") + } + menuView.onMenuHoverListener = onHoverListener viewHost.setView(menuView.rootView, lp) } @@ -198,56 +219,6 @@ class MaximizeMenu( } /** - * Called when a [MotionEvent.ACTION_HOVER_ENTER] is triggered on any of the menu's views. - * - * TODO(b/346440693): this is only needed for the left/right snap options that don't support - * selector states to manage its hover state. Look into whether that can be added to avoid - * manually tracking hover enter/exit motion events. Also because those button colors/states - * aren't updating correctly for pressed, focused and selected states. - * See also [onMaximizeMenuHoverMove] and [onMaximizeMenuHoverExit]. - */ - fun onMaximizeMenuHoverEnter(viewId: Int, ev: MotionEvent) { - setSnapButtonsColorOnHover(viewId, ev) - } - - /** Called when a [MotionEvent.ACTION_HOVER_MOVE] is triggered on any of the menu's views. */ - fun onMaximizeMenuHoverMove(viewId: Int, ev: MotionEvent) { - setSnapButtonsColorOnHover(viewId, ev) - } - - /** Called when a [MotionEvent.ACTION_HOVER_EXIT] is triggered on any of the menu's views. */ - fun onMaximizeMenuHoverExit(id: Int, ev: MotionEvent) { - val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return - val snapOptionsHeight = maximizeMenuView?.snapOptionsHeight ?: return - val inSnapMenuBounds = ev.x >= 0 && ev.x <= snapOptionsWidth && - ev.y >= 0 && ev.y <= snapOptionsHeight - - if (id == R.id.maximize_menu_snap_menu_layout && !inSnapMenuBounds) { - // After exiting the snap menu layout area, checks to see that user is not still - // hovering within the snap menu layout bounds which would indicate that the user is - // hovering over a snap button within the snap menu layout rather than having exited. - maximizeMenuView?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.NONE) - } - } - - private fun setSnapButtonsColorOnHover(viewId: Int, ev: MotionEvent) { - val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return - val snapMenuCenter = snapOptionsWidth / 2 - when { - viewId == R.id.maximize_menu_snap_left_button || - (viewId == R.id.maximize_menu_snap_menu_layout && ev.x <= snapMenuCenter) -> { - maximizeMenuView - ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.LEFT) - } - viewId == R.id.maximize_menu_snap_right_button || - (viewId == R.id.maximize_menu_snap_menu_layout && ev.x > snapMenuCenter) -> { - maximizeMenuView - ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.RIGHT) - } - } - } - - /** * The view within the Maximize Menu, presents maximize, restore and snap-to-side options for * resizing a Task. */ @@ -255,12 +226,11 @@ class MaximizeMenu( context: Context, private val menuHeight: Int, private val menuPadding: Int, - onClickListener: OnClickListener, - onTouchListener: OnTouchListener, - onGenericMotionListener: OnGenericMotionListener, ) { - val rootView: View = LayoutInflater.from(context) - .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) + val rootView = LayoutInflater.from(context) + .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) as ViewGroup + private val container = requireViewById(R.id.container) + private val overlay = requireViewById(R.id.maximize_menu_overlay) private val maximizeText = requireViewById(R.id.maximize_menu_maximize_window_text) as TextView private val maximizeButton = @@ -285,30 +255,63 @@ class MaximizeMenu( private val fillRadius = context.resources .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius) + private val hoverTempRect = Rect() private val openMenuAnimatorSet = AnimatorSet() private lateinit var taskInfo: RunningTaskInfo private lateinit var style: MenuStyle - /** The width of the snap menu option view, including both left and right snaps. */ - val snapOptionsWidth: Int - get() = snapButtonsLayout.width - /** The height of the snap menu option view, including both left and right snaps .*/ - val snapOptionsHeight: Int - get() = snapButtonsLayout.height + /** Invoked when the maximize or restore option is clicked. */ + var onMaximizeClickListener: (() -> Unit)? = null + /** Invoked when the left snap option is clicked. */ + var onLeftSnapClickListener: (() -> Unit)? = null + /** Invoked when the right snap option is clicked. */ + var onRightSnapClickListener: (() -> Unit)? = null + /** Invoked whenever the hover state of the menu changes. */ + var onMenuHoverListener: ((Boolean) -> Unit)? = null init { - // TODO(b/346441962): encapsulate menu hover enter/exit logic inside this class and - // expose only what is actually relevant to outside classes so that specific checks - // against resource IDs aren't needed outside this class. - rootView.setOnGenericMotionListener(onGenericMotionListener) - rootView.setOnTouchListener(onTouchListener) - maximizeButton.setOnClickListener(onClickListener) - maximizeButton.setOnGenericMotionListener(onGenericMotionListener) - snapRightButton.setOnClickListener(onClickListener) - snapRightButton.setOnGenericMotionListener(onGenericMotionListener) - snapLeftButton.setOnClickListener(onClickListener) - snapLeftButton.setOnGenericMotionListener(onGenericMotionListener) - snapButtonsLayout.setOnGenericMotionListener(onGenericMotionListener) + overlay.setOnHoverListener { _, event -> + // The overlay covers the entire menu, so it's a convenient way to monitor whether + // the menu is hovered as a whole or not. + when (event.action) { + ACTION_HOVER_ENTER -> onMenuHoverListener?.invoke(true) + ACTION_HOVER_EXIT -> onMenuHoverListener?.invoke(false) + } + + // Also check if the hover falls within the snap options layout, to manually + // set the left/right state based on the event's position. + // TODO(b/346440693): this manual hover tracking is needed for left/right snap + // because its view/background(s) don't support selector states. Look into whether + // that can be added to avoid manual tracking. Also because these button + // colors/state logic is only being applied on hover events, but there's pressed, + // focused and selected states that should be responsive too. + val snapLayoutBoundsRelToOverlay = hoverTempRect.also { rect -> + snapButtonsLayout.getDrawingRect(rect) + rootView.offsetDescendantRectToMyCoords(snapButtonsLayout, rect) + } + if (event.action == ACTION_HOVER_ENTER || event.action == ACTION_HOVER_MOVE) { + if (snapLayoutBoundsRelToOverlay.contains(event.x.toInt(), event.y.toInt())) { + // Hover is inside the snap layout, anything left of center is the left + // snap, and anything right of center is right snap. + val layoutCenter = snapLayoutBoundsRelToOverlay.centerX() + if (event.x < layoutCenter) { + updateSplitSnapSelection(SnapToHalfSelection.LEFT) + } else { + updateSplitSnapSelection(SnapToHalfSelection.RIGHT) + } + } else { + // Any other hover is outside the snap layout, so neither is selected. + updateSplitSnapSelection(SnapToHalfSelection.NONE) + } + } + + // Don't consume the event to allow child views to receive the event too. + return@setOnHoverListener false + } + + maximizeButton.setOnClickListener { onMaximizeClickListener?.invoke() } + snapRightButton.setOnClickListener { onRightSnapClickListener?.invoke() } + snapLeftButton.setOnClickListener { onLeftSnapClickListener?.invoke() } // To prevent aliasing. maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) @@ -351,7 +354,7 @@ class MaximizeMenu( val value = animatedValue as Float val topPadding = menuPadding - ((1 - value) * menuHeight).toInt() - rootView.setPadding(menuPadding, topPadding, + container.setPadding(menuPadding, topPadding, menuPadding, menuPadding) } }, @@ -410,7 +413,7 @@ class MaximizeMenu( } /** Update the view state to a new snap to half selection. */ - fun updateSplitSnapSelection(selection: SnapToHalfSelection) { + private fun updateSplitSnapSelection(selection: SnapToHalfSelection) { when (selection) { SnapToHalfSelection.NONE -> deactivateSnapOptions() SnapToHalfSelection.LEFT -> activateSnapOption(activateLeft = true) @@ -638,13 +641,41 @@ class MaximizeMenu( private const val ELEVATION_ANIMATION_DURATION_MS = 50L private const val CONTROLS_ALPHA_ANIMATION_DELAY_MS = 33L private const val MENU_Z_TRANSLATION = 1f - fun isMaximizeMenuView(@IdRes viewId: Int): Boolean { - return viewId == R.id.maximize_menu || - viewId == R.id.maximize_menu_maximize_button || - viewId == R.id.maximize_menu_snap_left_button || - viewId == R.id.maximize_menu_snap_right_button || - viewId == R.id.maximize_menu_snap_menu_layout || - viewId == R.id.maximize_menu_snap_menu_layout - } + } +} + +/** A factory interface to create a [MaximizeMenu]. */ +interface MaximizeMenuFactory { + fun create( + syncQueue: SyncTransactionQueue, + rootTdaOrganizer: RootTaskDisplayAreaOrganizer, + displayController: DisplayController, + taskInfo: RunningTaskInfo, + decorWindowContext: Context, + menuPosition: PointF, + transactionSupplier: Supplier<Transaction> + ): MaximizeMenu +} + +/** A [MaximizeMenuFactory] implementation that creates a [MaximizeMenu]. */ +object DefaultMaximizeMenuFactory : MaximizeMenuFactory { + override fun create( + syncQueue: SyncTransactionQueue, + rootTdaOrganizer: RootTaskDisplayAreaOrganizer, + displayController: DisplayController, + taskInfo: RunningTaskInfo, + decorWindowContext: Context, + menuPosition: PointF, + transactionSupplier: Supplier<Transaction> + ): MaximizeMenu { + return MaximizeMenu( + syncQueue, + rootTdaOrganizer, + displayController, + taskInfo, + decorWindowContext, + menuPosition, + transactionSupplier + ) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt new file mode 100644 index 000000000000..14b9e7f71622 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/OnTaskActionClickListener.kt @@ -0,0 +1,27 @@ +/* + * 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.common + +/** A callback to be invoked when a Task's window decor element is clicked. */ +fun interface OnTaskActionClickListener { + /** + * Called when a task's decor element has been clicked. + * + * @param taskId the id of the task. + * @param tag a readable identifier for the element. + */ + fun onClick(taskId: Int, tag: String) +} 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 ca1e3f173e24..4c94c2933383 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 @@ -67,6 +67,7 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopTasksController +import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.shared.DesktopModeStatus import com.android.wm.shell.sysui.KeyguardChangeListener @@ -75,6 +76,7 @@ import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit 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 @@ -82,6 +84,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.anyInt @@ -518,6 +521,99 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } } + @Test + fun testOnDecorMaximizedOrRestored_togglesTaskSize() { + val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM)) + onTaskOpening(decor.mTaskInfo) + val maxOrRestoreListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java) + .let { captor -> + verify(decor).setOnMaximizeOrRestoreClickListener(captor.capture()) + return@let captor.value + } + + maxOrRestoreListener.onClick(decor.mTaskInfo.taskId, "test") + + verify(mockDesktopTasksController).toggleDesktopTaskSize(decor.mTaskInfo) + } + + @Test + fun testOnDecorMaximizedOrRestored_closesMenus() { + val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM)) + onTaskOpening(decor.mTaskInfo) + val maxOrRestoreListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java) + .let { captor -> + verify(decor).setOnMaximizeOrRestoreClickListener(captor.capture()) + return@let captor.value + } + + maxOrRestoreListener.onClick(decor.mTaskInfo.taskId, "test") + + verify(decor).closeHandleMenu() + verify(decor).closeMaximizeMenu() + } + + @Test + fun testOnDecorSnappedLeft_snapResizes() { + val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM)) + onTaskOpening(decor.mTaskInfo) + val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java) + .let { captor -> + verify(decor).setOnLeftSnapClickListener(captor.capture()) + return@let captor.value + } + + snapLeftListener.onClick(decor.mTaskInfo.taskId, "test") + + verify(mockDesktopTasksController).snapToHalfScreen(decor.mTaskInfo, SnapPosition.LEFT) + } + + @Test + fun testOnDecorSnappedLeft_closeMenus() { + val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM)) + onTaskOpening(decor.mTaskInfo) + val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java) + .let { captor -> + verify(decor).setOnLeftSnapClickListener(captor.capture()) + return@let captor.value + } + + snapLeftListener.onClick(decor.mTaskInfo.taskId, "test") + + verify(decor).closeHandleMenu() + verify(decor).closeMaximizeMenu() + } + + @Test + fun testOnDecorSnappedRight_snapResizes() { + val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM)) + onTaskOpening(decor.mTaskInfo) + val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java) + .let { captor -> + verify(decor).setOnRightSnapClickListener(captor.capture()) + return@let captor.value + } + + snapLeftListener.onClick(decor.mTaskInfo.taskId, "test") + + verify(mockDesktopTasksController).snapToHalfScreen(decor.mTaskInfo, SnapPosition.RIGHT) + } + + @Test + fun testOnDecorSnappedRight_closeMenus() { + val decor = setUpMockDecorationForTask(createTask(windowingMode = WINDOWING_MODE_FREEFORM)) + onTaskOpening(decor.mTaskInfo) + val snapLeftListener = ArgumentCaptor.forClass(OnTaskActionClickListener::class.java) + .let { captor -> + verify(decor).setOnRightSnapClickListener(captor.capture()) + return@let captor.value + } + + snapLeftListener.onClick(decor.mTaskInfo.taskId, "test") + + verify(decor).closeHandleMenu() + verify(decor).closeMaximizeMenu() + } + private fun onTaskOpening(task: RunningTaskInfo, leash: SurfaceControl = SurfaceControl()) { desktopModeWindowDecorViewModel.onTaskOpening( task, 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 46c158908226..36e8a4671a46 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 @@ -24,9 +24,14 @@ import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlTransaction; +import static com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.CLOSE_MAXIMIZE_MENU_DELAY_MS; import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.doReturn; @@ -38,11 +43,13 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.content.ComponentName; +import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.PointF; import android.os.Handler; import android.os.SystemProperties; import android.platform.test.annotations.DisableFlags; @@ -62,6 +69,7 @@ import android.view.View; import android.view.WindowManager; import android.window.WindowContainerTransaction; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; @@ -76,6 +84,10 @@ import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.shared.DesktopModeStatus; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams; +import com.android.wm.shell.windowdecor.common.OnTaskActionClickListener; + +import kotlin.Unit; +import kotlin.jvm.functions.Function1; import org.junit.After; import org.junit.Before; @@ -84,6 +96,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.quality.Strictness; @@ -112,8 +125,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @Mock private ShellTaskOrganizer mMockShellTaskOrganizer; @Mock - private Handler mMockHandler; - @Mock private Choreographer mMockChoreographer; @Mock private SyncTransactionQueue mMockSyncQueue; @@ -131,13 +142,18 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private WindowDecoration.SurfaceControlViewHostFactory mMockSurfaceControlViewHostFactory; @Mock private TypedArray mMockRoundedCornersRadiusArray; - @Mock private TestTouchEventListener mMockTouchEventListener; @Mock private DesktopModeWindowDecoration.ExclusionRegionListener mMockExclusionRegionListener; @Mock private PackageManager mMockPackageManager; + @Mock + private Handler mMockHandler; + @Captor + private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener; + @Captor + private ArgumentCaptor<Runnable> mCloseMaxMenuRunnable; private final InsetsState mInsetsState = new InsetsState(); private SurfaceControl.Transaction mMockTransaction; @@ -459,6 +475,92 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(mMockHandler).removeCallbacks(runnableArgument.getValue()); } + @Test + public void createMaximizeMenu_showsMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + assertFalse(decoration.isMaximizeMenuActive()); + + createMaximizeMenu(decoration, menu); + + assertTrue(decoration.isMaximizeMenuActive()); + } + + @Test + public void maximizeMenu_unHoversMenu_schedulesCloseMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + decoration.setAppHeaderMaximizeButtonHovered(false); + createMaximizeMenu(decoration, menu); + + mOnMaxMenuHoverChangeListener.getValue().invoke(false); + + verify(mMockHandler) + .postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS)); + + mCloseMaxMenuRunnable.getValue().run(); + verify(menu).close(); + assertFalse(decoration.isMaximizeMenuActive()); + } + + @Test + public void maximizeMenu_unHoversButton_schedulesCloseMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + decoration.setAppHeaderMaximizeButtonHovered(true); + createMaximizeMenu(decoration, menu); + + decoration.setAppHeaderMaximizeButtonHovered(false); + + verify(mMockHandler) + .postDelayed(mCloseMaxMenuRunnable.capture(), eq(CLOSE_MAXIMIZE_MENU_DELAY_MS)); + + mCloseMaxMenuRunnable.getValue().run(); + verify(menu).close(); + assertFalse(decoration.isMaximizeMenuActive()); + } + + @Test + public void maximizeMenu_hoversMenu_cancelsCloseMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + createMaximizeMenu(decoration, menu); + + mOnMaxMenuHoverChangeListener.getValue().invoke(true); + + verify(mMockHandler).removeCallbacks(any()); + } + + @Test + public void maximizeMenu_hoversButton_cancelsCloseMenu() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(true /* visible */); + final MaximizeMenu menu = mock(MaximizeMenu.class); + final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, + new FakeMaximizeMenuFactory(menu)); + createMaximizeMenu(decoration, menu); + + decoration.setAppHeaderMaximizeButtonHovered(true); + + verify(mMockHandler).removeCallbacks(any()); + } + + private void createMaximizeMenu(DesktopModeWindowDecoration decoration, MaximizeMenu menu) { + final OnTaskActionClickListener l = (taskId, tag) -> {}; + decoration.setOnMaximizeOrRestoreClickListener(l); + decoration.setOnLeftSnapClickListener(l); + decoration.setOnRightSnapClickListener(l); + decoration.createMaximizeMenu(); + verify(menu).show(any(), any(), any(), mOnMaxMenuHoverChangeListener.capture()); + } + private void fillRoundedCornersResources(int fillValue) { when(mMockRoundedCornersRadiusArray.getDimensionPixelSize(anyInt(), anyInt())) .thenReturn(fillValue); @@ -479,12 +581,19 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private DesktopModeWindowDecoration createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo) { - DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext, + return createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory()); + } + + private DesktopModeWindowDecoration createWindowDecoration( + ActivityManager.RunningTaskInfo taskInfo, + MaximizeMenuFactory maximizeMenuFactory) { + final DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer, SurfaceControl.Builder::new, mMockTransactionSupplier, WindowContainerTransaction::new, SurfaceControl::new, - mMockSurfaceControlViewHostFactory); + mMockSurfaceControlViewHostFactory, + maximizeMenuFactory); windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener); windowDecor.setExclusionRegionListener(mMockExclusionRegionListener); @@ -541,4 +650,27 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { return false; } } + + private static final class FakeMaximizeMenuFactory implements MaximizeMenuFactory { + private final MaximizeMenu mMaximizeMenu; + + FakeMaximizeMenuFactory() { + this(mock(MaximizeMenu.class)); + } + + FakeMaximizeMenuFactory(MaximizeMenu menu) { + mMaximizeMenu = menu; + } + + @NonNull + @Override + public MaximizeMenu create(@NonNull SyncTransactionQueue syncQueue, + @NonNull RootTaskDisplayAreaOrganizer rootTdaOrganizer, + @NonNull DisplayController displayController, + @NonNull ActivityManager.RunningTaskInfo taskInfo, + @NonNull Context decorWindowContext, @NonNull PointF menuPosition, + @NonNull Supplier<SurfaceControl.Transaction> transactionSupplier) { + return mMaximizeMenu; + } + } } |