From 853a8e3ed9492b8fb987956ad160c387ed840dd2 Mon Sep 17 00:00:00 2001
From: Maryam Dehaini <mdehaini@google.com>
Date: Fri, 12 Jan 2024 13:43:31 -0800
Subject: [PATCH] Add hover support to maximize menu

Open maximize menu when user hovers over maximize window button in
caption.

Bug: 312748319
Test: Manual testing
Change-Id: Ie5bc94c02c5555ea99e6e42f9f6a00c65e348172
---
 ...ode_caption_button_color_selector_dark.xml |  21 ++++
 ...de_caption_button_color_selector_light.xml |  21 ++++
 .../Shell/res/drawable/circular_progress.xml  |  33 ++++++
 .../Shell/res/drawable/rounded_button.xml     |  19 ++++
 ...desktop_mode_app_controls_window_decor.xml |  16 +--
 ...esktop_mode_window_decor_maximize_menu.xml |   1 +
 .../Shell/res/layout/maximize_menu_button.xml |  36 ++++++
 .../WindowManager/Shell/res/values/colors.xml |   5 +
 .../DesktopModeWindowDecorViewModel.java      |  53 +++++++--
 .../DesktopModeWindowDecoration.java          |  36 +++++-
 .../shell/windowdecor/MaximizeButtonView.kt   | 106 ++++++++++++++++++
 .../wm/shell/windowdecor/MaximizeMenu.kt      |  36 +++++-
 ...deAppControlsWindowDecorationViewHolder.kt |  28 ++++-
 13 files changed, 381 insertions(+), 30 deletions(-)
 create mode 100644 libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_dark.xml
 create mode 100644 libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_light.xml
 create mode 100644 libs/WindowManager/Shell/res/drawable/circular_progress.xml
 create mode 100644 libs/WindowManager/Shell/res/drawable/rounded_button.xml
 create mode 100644 libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
 create mode 100644 libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt

diff --git a/libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_dark.xml b/libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_dark.xml
new file mode 100644
index 000000000000..52a59671baa1
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_dark.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_hovered="true"
+        android:color="@color/desktop_mode_caption_button_on_hover_dark"/>
+    <item android:color="@color/desktop_mode_caption_button"/>
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_light.xml b/libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_light.xml
new file mode 100644
index 000000000000..6d8a51cd6f8f
--- /dev/null
+++ b/libs/WindowManager/Shell/res/color/desktop_mode_caption_button_color_selector_light.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_hovered="true"
+        android:color="@color/desktop_mode_caption_button_on_hover_light"/>
+    <item android:color="@color/desktop_mode_caption_button"/>
+</selector>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/circular_progress.xml b/libs/WindowManager/Shell/res/drawable/circular_progress.xml
new file mode 100644
index 000000000000..948264579e1d
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/circular_progress.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@android:id/progress">
+        <rotate
+            android:pivotX="50%"
+            android:pivotY="50%"
+            android:fromDegrees="275"
+            android:toDegrees="275">
+            <shape
+                android:shape="ring"
+                android:thickness="3dp"
+                android:innerRadius="17dp"
+                android:useLevel="true">
+            </shape>
+        </rotate>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/rounded_button.xml b/libs/WindowManager/Shell/res/drawable/rounded_button.xml
new file mode 100644
index 000000000000..17a0bab56a74
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/rounded_button.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <corners  android:radius="20dp" />
+</shape>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
index e4f793c2665b..d1b1af3e77ab 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
@@ -74,17 +74,11 @@
         android:layout_height="40dp"
         android:layout_weight="1"/>
 
-    <ImageButton
-        android:id="@+id/maximize_window"
-        android:layout_width="40dp"
-        android:layout_height="40dp"
-        android:padding="9dp"
-        android:layout_marginEnd="8dp"
-        android:contentDescription="@string/maximize_button_text"
-        android:src="@drawable/decor_desktop_mode_maximize_button_dark"
-        android:scaleType="fitCenter"
-        android:gravity="end"
-        android:background="@null"/>
+    <com.android.wm.shell.windowdecor.MaximizeButtonView
+        android:id="@+id/maximize_button_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="end"/>
 
     <ImageButton
         android:id="@+id/close_window"
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 0db72f7be8e6..dbfd6e5d8d94 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
@@ -15,6 +15,7 @@
   ~ limitations under the License.
   -->
 <LinearLayout xmlns:android="http://schemas.android.com/apk/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"
diff --git a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
new file mode 100644
index 000000000000..bb6efcec1a70
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <ProgressBar
+        android:id="@+id/progress_bar"
+        style="?android:attr/progressBarStyleHorizontal"
+        android:progressDrawable="@drawable/circular_progress"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        android:indeterminate="false"
+        android:visibility="invisible"/>
+
+    <ImageButton
+        android:id="@+id/maximize_window"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        android:padding="9dp"
+        android:contentDescription="@string/maximize_button_text"
+        android:src="@drawable/decor_desktop_mode_maximize_button_dark"
+        android:scaleType="fitCenter"
+        android:background="@drawable/rounded_button"/>
+</merge>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
index fae71efe3b39..758dbfd5f3c5 100644
--- a/libs/WindowManager/Shell/res/values/colors.xml
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -66,4 +66,9 @@
     <color name="desktop_mode_maximize_menu_button_outline">#797869</color>
     <color name="desktop_mode_maximize_menu_button_outline_on_hover">#606219</color>
     <color name="desktop_mode_maximize_menu_button_on_hover">#E7E790</color>
+    <color name="desktop_mode_maximize_menu_progress_light">#33000000</color>
+    <color name="desktop_mode_maximize_menu_progress_dark">#33FFFFFF</color>
+    <color name="desktop_mode_caption_button_on_hover_light">#11000000</color>
+    <color name="desktop_mode_caption_button_on_hover_dark">#11FFFFFF</color>
+    <color name="desktop_mode_caption_button">#00000000</color>
 </resources>
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 57e964f15d50..5b8ffb30dc4a 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
@@ -22,6 +22,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+import static android.view.MotionEvent.ACTION_HOVER_ENTER;
+import static android.view.MotionEvent.ACTION_HOVER_EXIT;
 import static android.view.WindowInsets.Type.statusBars;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -311,8 +313,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
 
     private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
             implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
-            DragDetector.MotionEventHandler {
-
+            View.OnGenericMotionListener , DragDetector.MotionEventHandler {
+        private static final int CLOSE_MAXIMIZE_MENU_DELAY_MS = 150;
         private final int mTaskId;
         private final WindowContainerToken mTaskToken;
         private final DragPositioningCallback mDragPositioningCallback;
@@ -323,6 +325,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
         private boolean mHasLongClicked;
         private boolean mShouldClick;
         private int mDragPointerId = -1;
+        private final Runnable mCloseMaximizeWindowRunnable;
 
         private DesktopModeTouchEventListener(
                 RunningTaskInfo taskInfo,
@@ -332,6 +335,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
             mDragPositioningCallback = dragPositioningCallback;
             mDragDetector = new DragDetector(this);
             mGestureDetector = new GestureDetector(mContext, this);
+            mCloseMaximizeWindowRunnable = () -> {
+                final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+                if (decoration == null) return;
+                decoration.closeMaximizeMenu();
+            };
         }
 
         @Override
@@ -387,13 +395,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
                     mDesktopTasksController.ifPresent(c -> c.moveToNextDisplay(mTaskId));
                 }
             } else if (id == R.id.maximize_window) {
-                if (decoration.isMaximizeMenuActive()) {
-                    decoration.closeMaximizeMenu();
-                    return;
-                }
                 final RunningTaskInfo taskInfo = decoration.mTaskInfo;
-                mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo));
                 decoration.closeHandleMenu();
+                decoration.closeMaximizeMenu();
+                mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo));
             } else if (id == R.id.maximize_menu_maximize_button) {
                 final RunningTaskInfo taskInfo = decoration.mTaskInfo;
                 mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo));
@@ -460,6 +465,36 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
             return false;
         }
 
+        @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);
+                }
+                return true;
+            } 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);
+                }
+                return true;
+            }
+            return false;
+        }
+
         private void moveTaskToFront(RunningTaskInfo taskInfo) {
             if (!taskInfo.isFocused) {
                 mDesktopTasksController.ifPresent(c -> c.moveTaskToFront(taskInfo));
@@ -990,7 +1025,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
                 new DesktopModeTouchEventListener(taskInfo, dragPositioningCallback);
 
         windowDecoration.setCaptionListeners(
-                touchEventListener, touchEventListener, touchEventListener);
+                touchEventListener, touchEventListener, touchEventListener, touchEventListener);
         windowDecoration.setExclusionRegionListener(mExclusionRegionListener);
         windowDecoration.setDragPositioningCallback(dragPositioningCallback);
         windowDecoration.setDragDetector(touchEventListener.mDragDetector);
@@ -1036,6 +1071,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
                 return;
             }
             decoration.showResizeVeil(t, bounds);
+            decoration.setAnimatingTaskResize(true);
         }
 
         @Override
@@ -1050,6 +1086,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
             final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId);
             if (decoration == null) return;
             decoration.hideResizeVeil();
+            decoration.setAnimatingTaskResize(false);
         }
     }
 
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 185365b2a501..74f460bf1226 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
@@ -60,6 +60,8 @@ import com.android.wm.shell.windowdecor.viewholder.DesktopModeAppControlsWindowD
 import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder;
 import com.android.wm.shell.windowdecor.viewholder.DesktopModeWindowDecorationViewHolder;
 
+import kotlin.Unit;
+
 import java.util.function.Supplier;
 
 /**
@@ -79,6 +81,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
     private View.OnClickListener mOnCaptionButtonClickListener;
     private View.OnTouchListener mOnCaptionTouchListener;
     private View.OnLongClickListener mOnCaptionLongClickListener;
+    private View.OnGenericMotionListener mOnCaptionGenericMotionListener;
     private DragPositioningCallback mDragPositioningCallback;
     private DragResizeInputListener mDragResizeListener;
     private DragDetector mDragDetector;
@@ -152,10 +155,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
     void setCaptionListeners(
             View.OnClickListener onCaptionButtonClickListener,
             View.OnTouchListener onCaptionTouchListener,
-            View.OnLongClickListener onLongClickListener) {
+            View.OnLongClickListener onLongClickListener,
+            View.OnGenericMotionListener onGenericMotionListener) {
         mOnCaptionButtonClickListener = onCaptionButtonClickListener;
         mOnCaptionTouchListener = onCaptionTouchListener;
         mOnCaptionLongClickListener = onLongClickListener;
+        mOnCaptionGenericMotionListener = onGenericMotionListener;
     }
 
     void setExclusionRegionListener(ExclusionRegionListener exclusionRegionListener) {
@@ -225,9 +230,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
                         mOnCaptionTouchListener,
                         mOnCaptionButtonClickListener,
                         mOnCaptionLongClickListener,
+                        mOnCaptionGenericMotionListener,
                         mAppName,
-                        mAppIconBitmap
-                );
+                        mAppIconBitmap,
+                        () -> {
+                            if (!isMaximizeMenuActive()) {
+                                createMaximizeMenu();
+                            }
+                            return Unit.INSTANCE;
+                        });
             } else {
                 throw new IllegalArgumentException("Unexpected layout resource id");
             }
@@ -548,7 +559,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
      */
     void createMaximizeMenu() {
         mMaximizeMenu = new MaximizeMenu(mSyncQueue, mRootTaskDisplayAreaOrganizer,
-                mDisplayController, mTaskInfo, mOnCaptionButtonClickListener, mContext,
+                mDisplayController, mTaskInfo, mOnCaptionButtonClickListener,
+                mOnCaptionGenericMotionListener, mOnCaptionTouchListener, mContext,
                 calculateMaximizeMenuPosition(), mSurfaceControlTransactionSupplier);
         mMaximizeMenu.show();
     }
@@ -776,6 +788,22 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
         return R.id.desktop_mode_caption;
     }
 
+    void setAnimatingTaskResize(boolean animatingTaskResize) {
+        if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) return;
+        ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder)
+                .setAnimatingTaskResize(animatingTaskResize);
+    }
+
+    void onMaximizeWindowHoverExit() {
+        ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder)
+                .onMaximizeWindowHoverExit();
+    }
+
+    void onMaximizeWindowHoverEnter() {
+        ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder)
+                .onMaximizeWindowHoverEnter();
+    }
+
     @Override
     public String toString() {
         return "{"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
new file mode 100644
index 000000000000..b2f8cfdbfb7a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.content.res.ColorStateList
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageButton
+import android.widget.ProgressBar
+import androidx.core.animation.doOnEnd
+import androidx.core.animation.doOnStart
+import androidx.core.content.ContextCompat
+import com.android.wm.shell.R
+
+private const val OPEN_MAXIMIZE_MENU_DELAY_ON_HOVER_MS = 350
+private const val MAX_DRAWABLE_ALPHA = 255
+
+class MaximizeButtonView(
+        context: Context,
+        attrs: AttributeSet
+) : FrameLayout(context, attrs) {
+    lateinit var onHoverAnimationFinishedListener: () -> Unit
+    private val hoverProgressAnimatorSet = AnimatorSet()
+    var hoverDisabled = false
+
+    private val progressBar: ProgressBar
+    private val maximizeWindow: ImageButton
+
+    init {
+        LayoutInflater.from(context).inflate(R.layout.maximize_menu_button, this, true)
+
+        progressBar = requireViewById(R.id.progress_bar)
+        maximizeWindow = requireViewById(R.id.maximize_window)
+    }
+
+    fun startHoverAnimation() {
+        if (hoverDisabled) return
+        if (hoverProgressAnimatorSet.isRunning) {
+            cancelHoverAnimation()
+        }
+
+        maximizeWindow.background.alpha = 0
+
+        hoverProgressAnimatorSet.playSequentially(
+                ValueAnimator.ofInt(0, MAX_DRAWABLE_ALPHA)
+                        .setDuration(50)
+                        .apply {
+                            addUpdateListener {
+                                maximizeWindow.background.alpha = animatedValue as Int
+                            }
+                        },
+                ObjectAnimator.ofInt(progressBar, "progress", 100)
+                        .setDuration(OPEN_MAXIMIZE_MENU_DELAY_ON_HOVER_MS.toLong())
+                        .apply {
+                            doOnStart {
+                                progressBar.setProgress(0, false)
+                                progressBar.visibility = View.VISIBLE
+                            }
+                            doOnEnd {
+                                progressBar.visibility = View.INVISIBLE
+                                onHoverAnimationFinishedListener()
+                            }
+                        }
+        )
+        hoverProgressAnimatorSet.start()
+    }
+
+    fun cancelHoverAnimation() {
+        hoverProgressAnimatorSet.removeAllListeners()
+        hoverProgressAnimatorSet.cancel()
+        progressBar.visibility = View.INVISIBLE
+    }
+
+    fun setAnimationTints(darkMode: Boolean) {
+        if (darkMode) {
+            progressBar.progressTintList = ColorStateList.valueOf(
+                    resources.getColor(R.color.desktop_mode_maximize_menu_progress_dark))
+            maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context,
+                    R.color.desktop_mode_caption_button_color_selector_dark))
+        } else {
+            progressBar.progressTintList = ColorStateList.valueOf(
+                    resources.getColor(R.color.desktop_mode_maximize_menu_progress_light))
+            maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context,
+                    R.color.desktop_mode_caption_button_color_selector_light))
+        }
+    }
+}
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 794b357c9f16..b82f7ca47ef3 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
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.windowdecor
 
+import android.annotation.IdRes
 import android.app.ActivityManager.RunningTaskInfo
 import android.content.Context
 import android.content.res.Resources
@@ -27,6 +28,8 @@ import android.view.SurfaceControl
 import android.view.SurfaceControl.Transaction
 import android.view.SurfaceControlViewHost
 import android.view.View.OnClickListener
+import android.view.View.OnGenericMotionListener
+import android.view.View.OnTouchListener
 import android.view.WindowManager
 import android.view.WindowlessWindowManager
 import android.widget.Button
@@ -49,6 +52,8 @@ class MaximizeMenu(
         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() }
@@ -142,15 +147,26 @@ class MaximizeMenu(
     private fun setupMaximizeMenu() {
         val maximizeMenuView = maximizeMenu?.mWindowViewHost?.view ?: return
 
-        maximizeMenuView.requireViewById<Button>(
+        maximizeMenuView.setOnGenericMotionListener(onGenericMotionListener)
+        maximizeMenuView.setOnTouchListener(onTouchListener)
+
+        val maximizeButton = maximizeMenuView.requireViewById<Button>(
                 R.id.maximize_menu_maximize_button
-        ).setOnClickListener(onClickListener)
-        maximizeMenuView.requireViewById<Button>(
+        )
+        maximizeButton.setOnClickListener(onClickListener)
+        maximizeButton.setOnGenericMotionListener(onGenericMotionListener)
+
+        val snapRightButton = maximizeMenuView.requireViewById<Button>(
                 R.id.maximize_menu_snap_right_button
-        ).setOnClickListener(onClickListener)
-        maximizeMenuView.requireViewById<Button>(
+        )
+        snapRightButton.setOnClickListener(onClickListener)
+        snapRightButton.setOnGenericMotionListener(onGenericMotionListener)
+
+        val snapLeftButton = maximizeMenuView.requireViewById<Button>(
                 R.id.maximize_menu_snap_left_button
-        ).setOnClickListener(onClickListener)
+        )
+        snapLeftButton.setOnClickListener(onClickListener)
+        snapLeftButton.setOnGenericMotionListener(onGenericMotionListener)
     }
 
     /**
@@ -173,4 +189,12 @@ class MaximizeMenu(
     private fun viewsLaidOut(): Boolean {
         return maximizeMenu?.mWindowViewHost?.view?.isLaidOut ?: false
     }
+
+    companion object {
+        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
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
index 2309c54b6591..7e5b9bd649f2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
@@ -21,6 +21,7 @@ import com.android.internal.R.attr.materialColorSurfaceContainerHigh
 import com.android.internal.R.attr.materialColorSurfaceContainerLow
 import com.android.internal.R.attr.materialColorSurfaceDim
 import com.android.wm.shell.R
+import com.android.wm.shell.windowdecor.MaximizeButtonView
 
 /**
  * A desktop mode window decoration used when the window is floating (i.e. freeform). It hosts
@@ -32,8 +33,10 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder(
         onCaptionTouchListener: View.OnTouchListener,
         onCaptionButtonClickListener: View.OnClickListener,
         onLongClickListener: OnLongClickListener,
+        onCaptionGenericMotionListener: View.OnGenericMotionListener,
         appName: CharSequence,
-        appIconBitmap: Bitmap
+        appIconBitmap: Bitmap,
+        onMaximizeHoverAnimationFinishedListener: () -> Unit
 ) : DesktopModeWindowDecorationViewHolder(rootView) {
 
     private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
@@ -41,6 +44,8 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder(
     private val openMenuButton: View = rootView.requireViewById(R.id.open_menu_button)
     private val closeWindowButton: ImageButton = rootView.requireViewById(R.id.close_window)
     private val expandMenuButton: ImageButton = rootView.requireViewById(R.id.expand_menu_button)
+    private val maximizeButtonView: MaximizeButtonView =
+            rootView.requireViewById(R.id.maximize_button_view)
     private val maximizeWindowButton: ImageButton = rootView.requireViewById(R.id.maximize_window)
     private val appNameTextView: TextView = rootView.requireViewById(R.id.application_name)
     private val appIconImageView: ImageView = rootView.requireViewById(R.id.application_icon)
@@ -55,10 +60,13 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder(
         closeWindowButton.setOnClickListener(onCaptionButtonClickListener)
         maximizeWindowButton.setOnClickListener(onCaptionButtonClickListener)
         maximizeWindowButton.setOnTouchListener(onCaptionTouchListener)
+        maximizeWindowButton.setOnGenericMotionListener(onCaptionGenericMotionListener)
         maximizeWindowButton.onLongClickListener = onLongClickListener
         closeWindowButton.setOnTouchListener(onCaptionTouchListener)
         appNameTextView.text = appName
         appIconImageView.setImageBitmap(appIconBitmap)
+        maximizeButtonView.onHoverAnimationFinishedListener =
+                onMaximizeHoverAnimationFinishedListener
     }
 
     override fun bindData(taskInfo: RunningTaskInfo) {
@@ -73,12 +81,30 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder(
         maximizeWindowButton.imageAlpha = alpha
         closeWindowButton.imageAlpha = alpha
         expandMenuButton.imageAlpha = alpha
+
+        maximizeButtonView.setAnimationTints(isDarkMode())
     }
 
     override fun onHandleMenuOpened() {}
 
     override fun onHandleMenuClosed() {}
 
+    fun setAnimatingTaskResize(animatingTaskResize: Boolean) {
+        // If animating a task resize, cancel any running hover animations
+        if (animatingTaskResize) {
+            maximizeButtonView.cancelHoverAnimation()
+        }
+        maximizeButtonView.hoverDisabled = animatingTaskResize
+    }
+
+    fun onMaximizeWindowHoverExit() {
+        maximizeButtonView.cancelHoverAnimation()
+    }
+
+    fun onMaximizeWindowHoverEnter() {
+        maximizeButtonView.startHoverAnimation()
+    }
+
     @ColorInt
     private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo): Int {
         if (isTransparentBackgroundRequested(taskInfo)) {
-- 
GitLab