Merge "Listen for hover events over stashed taskbar." into udc-dev
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 6c7decd..5d2df70 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -292,6 +292,8 @@
     <dimen name="taskbar_stashed_small_screen">108dp</dimen>
     <dimen name="taskbar_unstash_input_area">316dp</dimen>
     <dimen name="taskbar_stashed_handle_height">4dp</dimen>
+    <dimen name="taskbar_stashed_screen_edge_hover_deadzone_height">10dp</dimen>
+    <dimen name="taskbar_stashed_below_hover_deadzone_height">1dp</dimen>
     <dimen name="taskbar_edu_horizontal_margin">112dp</dimen>
     <dimen name="taskbar_nav_buttons_width_kids">88dp</dimen>
     <dimen name="taskbar_nav_buttons_height_kids">40dp</dimen>
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 7e0530b..37f6284 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -929,6 +929,13 @@
     }
 
     /**
+     * Returns whether the taskbar is currently visually stashed.
+     */
+    public boolean isTaskbarStashed() {
+        return mControllers.taskbarStashController.isStashed();
+    }
+
+    /**
      * Called when we detect a long press in the nav region before passing the gesture slop.
      * @return Whether taskbar handled the long press, and thus should cancel the gesture.
      */
@@ -972,10 +979,23 @@
 
     /**
      * Called when we detect a motion down or up/cancel in the nav region while stashed.
+     *
      * @param animateForward Whether to animate towards the unstashed hint state or back to stashed.
      */
     public void startTaskbarUnstashHint(boolean animateForward) {
-        mControllers.taskbarStashController.startUnstashHint(animateForward);
+        // TODO(b/270395798): Clean up forceUnstash after removing long-press unstashing code.
+        startTaskbarUnstashHint(animateForward, /* forceUnstash = */ false);
+    }
+
+    /**
+     * Called when we detect a motion down or up/cancel in the nav region while stashed.
+     *
+     * @param animateForward Whether to animate towards the unstashed hint state or back to stashed.
+     * @param forceUnstash Whether we force the unstash hint.
+     */
+    public void startTaskbarUnstashHint(boolean animateForward, boolean forceUnstash) {
+        // TODO(b/270395798): Clean up forceUnstash after removing long-press unstashing code.
+        mControllers.taskbarStashController.startUnstashHint(animateForward, forceUnstash);
     }
 
     /**
@@ -1123,4 +1143,9 @@
     public int getTaskbarAllAppsScroll() {
         return mControllers.taskbarAllAppsController.getTaskbarAllAppsScroll();
     }
+
+    @VisibleForTesting
+    public float getStashedTaskbarScale() {
+        return mControllers.stashedHandleViewController.getStashedHandleHintScale().value;
+    }
 }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index b2f9378..5de5904 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -856,15 +856,18 @@
     /**
      * Creates and starts a partial unstash animation, hinting at the new state that will trigger
      * when long press is detected.
+     *
      * @param animateForward Whether we are going towards the new unstashed state or returning to
      *                       the stashed state.
+     * @param forceUnstash Whether we force the unstash hint to animate.
      */
-    public void startUnstashHint(boolean animateForward) {
+    protected void startUnstashHint(boolean animateForward, boolean forceUnstash) {
         if (!isStashed()) {
             // Already unstashed, no need to hint in that direction.
             return;
         }
-        if (!canCurrentlyManuallyUnstash()) {
+        // TODO(b/270395798): Clean up after removing long-press unstashing code path.
+        if (!canCurrentlyManuallyUnstash() && !forceUnstash) {
             // If any other flags are causing us to be stashed, long press won't cause us to
             // unstash, so don't hint that it will.
             return;
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
index 2373142..1cc6672 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashViaTouchController.kt
@@ -25,7 +25,7 @@
 import com.android.launcher3.touch.SingleAxisSwipeDetector.VERTICAL
 import com.android.launcher3.util.DisplayController
 import com.android.launcher3.util.TouchController
-import com.android.quickstep.inputconsumers.TaskbarStashInputConsumer
+import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer
 
 /**
  * A helper [TouchController] for [TaskbarDragLayerController], specifically to handle touch events
@@ -34,7 +34,7 @@
  *   or [MotionEvent.ACTION_OUTSIDE].
  * - Touches inside Transient Taskbar bounds will stash if it is detected as a swipe down gesture.
  *
- * Note: touches to *unstash* Taskbar are handled by [TaskbarStashInputConsumer].
+ * Note: touches to *unstash* Taskbar are handled by [TaskbarUnstashInputConsumer].
  */
 class TaskbarStashViaTouchController(val controllers: TaskbarControllers) : TouchController {
 
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index 64c9295..6b189cf 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -41,6 +41,7 @@
     int TYPE_ONE_HANDED = 1 << 11;
     int TYPE_TASKBAR_STASH = 1 << 12;
     int TYPE_STATUS_BAR = 1 << 13;
+    int TYPE_CURSOR_HOVER = 1 << 14;
 
     String[] NAMES = new String[] {
            "TYPE_NO_OP",                    // 0
@@ -57,6 +58,7 @@
             "TYPE_ONE_HANDED",              // 11
             "TYPE_TASKBAR_STASH",           // 12
             "TYPE_STATUS_BAR",              // 13
+            "TYPE_CURSOR_HOVER",            // 14
     };
 
     InputConsumer NO_OP = () -> TYPE_NO_OP;
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index ab3ae9f..4c9cf8b 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -116,6 +116,16 @@
                 return response;
             }
 
+            case TestProtocol.REQUEST_STASHED_TASKBAR_SCALE: {
+                runOnTISBinder(tisBinder -> {
+                    response.putFloat(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+                            tisBinder.getTaskbarManager()
+                                    .getCurrentActivityContext()
+                                    .getStashedTaskbarScale());
+                });
+                return response;
+            }
+
             case TestProtocol.REQUEST_TASKBAR_ALL_APPS_TOP_PADDING: {
                 return getTISBinderUIProperty(Bundle::putInt, tisBinder ->
                         tisBinder.getTaskbarManager()
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 6ea171e..66aeee7 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -29,6 +29,7 @@
 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
 import static com.android.quickstep.GestureState.DEFAULT_STATE;
 import static com.android.quickstep.GestureState.TrackpadGestureType.getTrackpadGestureType;
+import static com.android.quickstep.InputConsumer.TYPE_CURSOR_HOVER;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.FLAG_USING_OTHER_ACTIVITY_INPUT_CONSUMER;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_DOWN;
 import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.MOTION_MOVE;
@@ -110,7 +111,7 @@
 import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
 import com.android.quickstep.inputconsumers.StatusBarInputConsumer;
 import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer;
-import com.android.quickstep.inputconsumers.TaskbarStashInputConsumer;
+import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer;
 import com.android.quickstep.util.ActiveGestureLog;
 import com.android.quickstep.util.ActiveGestureLog.CompoundString;
 import com.android.quickstep.util.ProtoTracer;
@@ -641,12 +642,17 @@
                 TraceHelper.FLAG_ALLOW_BINDER_TRACKING);
 
         final int action = event.getActionMasked();
-        if (action == ACTION_DOWN) {
+        // Note this will create a new consumer every mouse click, as after ACTION_UP from the click
+        // an ACTION_HOVER_ENTER will fire as well.
+        boolean isHoverActionWithoutConsumer =
+                event.isHoverEvent() && (mUncheckedConsumer.getType() & TYPE_CURSOR_HOVER) == 0;
+        if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
             mRotationTouchHelper.setOrientationTransformIfNeeded(event);
 
-            if (!mDeviceState.isOneHandedModeActive()
+            if ((!mDeviceState.isOneHandedModeActive()
                     && mRotationTouchHelper.isInSwipeUpTouchRegion(event,
-                    mOverviewComponentObserver.getActivityInterface())) {
+                    mOverviewComponentObserver.getActivityInterface()))
+                    || isHoverActionWithoutConsumer) {
                 // Clone the previous gesture state since onConsumerAboutToBeSwitched might trigger
                 // onConsumerInactive and wipe the previous gesture state
                 GestureState prevGestureState = new GestureState(mGestureState);
@@ -723,6 +729,8 @@
             if (action == ACTION_POINTER_DOWN) {
                 mGestureState.setTrackpadGestureType(getTrackpadGestureType(event));
             }
+        } else if (event.isHoverEvent()) {
+            mUncheckedConsumer.onHoverEvent(event);
         } else {
             mUncheckedConsumer.onMotionEvent(event);
         }
@@ -846,7 +854,7 @@
                 base = tryCreateAssistantInputConsumer(base, newGestureState, event, reasonString);
             }
 
-            // If Taskbar is present, we listen for long press to unstash it.
+            // If Taskbar is present, we listen for long press or cursor hover events to unstash it.
             TaskbarActivityContext tac = mTaskbarManager.getCurrentActivityContext();
             if (tac != null) {
                 // Present always on large screen or on small screen w/ flag
@@ -857,8 +865,8 @@
                             .append(reasonPrefix)
                             .append(SUBSTRING_PREFIX)
                             .append("TaskbarActivityContext != null, "
-                                    + "using TaskbarStashInputConsumer");
-                    base = new TaskbarStashInputConsumer(this, base, mInputMonitorCompat, tac);
+                                    + "using TaskbarUnstashInputConsumer");
+                    base = new TaskbarUnstashInputConsumer(this, base, mInputMonitorCompat, tac);
                 }
             }
 
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
similarity index 69%
rename from quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
rename to quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index 51c2b48..65c825c 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -19,17 +19,20 @@
 
 import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
 import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
 import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TOUCHING;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.PointF;
+import android.graphics.Rect;
 import android.view.GestureDetector;
 import android.view.GestureDetector.SimpleOnGestureListener;
 import android.view.MotionEvent;
 
 import androidx.annotation.Nullable;
 
+import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.taskbar.TaskbarActivityContext;
@@ -40,9 +43,11 @@
 import com.android.systemui.shared.system.InputMonitorCompat;
 
 /**
- * Listens for a long press, and cancels the current gesture if that causes Taskbar to be unstashed.
+ * Listens for touch and hover events to unstash the Taskbar.
+ *
+ * <p>Cancels the current gesture if the long press causes the Taskbar to be unstashed.
  */
-public class TaskbarStashInputConsumer extends DelegateInputConsumer {
+public class TaskbarUnstashInputConsumer extends DelegateInputConsumer {
 
     private final TaskbarActivityContext mTaskbarActivityContext;
     private final GestureDetector mLongPressDetector;
@@ -64,9 +69,15 @@
 
     private final boolean mIsTransientTaskbar;
 
+    private boolean mIsStashedTaskbarHovered = false;
+    private final Rect mStashedTaskbarHandleBounds = new Rect();
+    private final Rect mBottomEdgeBounds = new Rect();
+    private final int mBottomScreenEdge;
+    private final int mStashedTaskbarBottomEdge;
+
     private final @Nullable TransitionCallback mTransitionCallback;
 
-    public TaskbarStashInputConsumer(Context context, InputConsumer delegate,
+    public TaskbarUnstashInputConsumer(Context context, InputConsumer delegate,
             InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext) {
         super(delegate, inputMonitor);
         mTaskbarActivityContext = taskbarActivityContext;
@@ -90,6 +101,11 @@
             }
         });
 
+        mBottomScreenEdge = res.getDimensionPixelSize(
+                R.dimen.taskbar_stashed_screen_edge_hover_deadzone_height);
+        mStashedTaskbarBottomEdge =
+                res.getDimensionPixelSize(R.dimen.taskbar_stashed_below_hover_deadzone_height);
+
         mTransitionCallback = mIsTransientTaskbar
                 ? taskbarActivityContext.getTranslationCallbacks()
                 : null;
@@ -97,7 +113,7 @@
 
     @Override
     public int getType() {
-        return TYPE_TASKBAR_STASH | mDelegate.getType();
+        return TYPE_TASKBAR_STASH | TYPE_CURSOR_HOVER | mDelegate.getType();
     }
 
     @Override
@@ -213,4 +229,73 @@
             }
         }
     }
+
+    /**
+     * Listen for hover events for the stashed taskbar.
+     *
+     * <p>When hovered over the stashed taskbar handle, show the unstash hint.
+     * <p>When the cursor is touching the bottom edge below the stashed taskbar, unstash it.
+     * <p>When the cursor is within a defined threshold of the screen's bottom edge outside of
+     * the stashed taskbar, unstash it.
+     */
+    @Override
+    public void onHoverEvent(MotionEvent ev) {
+        if (!ENABLE_CURSOR_HOVER_STATES.get() || mTaskbarActivityContext == null
+                || !mTaskbarActivityContext.isTaskbarStashed()) {
+            return;
+        }
+
+        if (mIsStashedTaskbarHovered) {
+            updateHoveredTaskbarState((int) ev.getX(), (int) ev.getY());
+        } else {
+            updateUnhoveredTaskbarState((int) ev.getX(), (int) ev.getY());
+        }
+    }
+
+    private void updateHoveredTaskbarState(int x, int y) {
+        DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
+        mStashedTaskbarHandleBounds.set(
+                (dp.widthPx - (int) mUnstashArea) / 2,
+                dp.heightPx - dp.stashedTaskbarHeight,
+                (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
+                dp.heightPx);
+        mBottomEdgeBounds.set(mStashedTaskbarHandleBounds);
+        mBottomEdgeBounds.top = dp.heightPx - mStashedTaskbarBottomEdge;
+
+        if (mBottomEdgeBounds.contains(x, y)) {
+            // If hovering stashed taskbar and then hover screen bottom edge, unstash it.
+            mTaskbarActivityContext.onSwipeToUnstashTaskbar();
+            mIsStashedTaskbarHovered = false;
+        } else if (!mStashedTaskbarHandleBounds.contains(x, y)) {
+            // If exit hovering stashed taskbar, remove hint.
+            startStashedTaskbarHover(/* isHovered = */ false);
+        }
+    }
+
+    private void updateUnhoveredTaskbarState(int x, int y) {
+        DeviceProfile dp = mTaskbarActivityContext.getDeviceProfile();
+        mStashedTaskbarHandleBounds.set(
+                (dp.widthPx - (int) mUnstashArea) / 2,
+                dp.heightPx - dp.stashedTaskbarHeight,
+                (int) (((dp.widthPx - mUnstashArea) / 2) + mUnstashArea),
+                dp.heightPx);
+        mBottomEdgeBounds.set(
+                0,
+                dp.heightPx - mBottomScreenEdge,
+                dp.widthPx,
+                dp.heightPx);
+
+        if (mStashedTaskbarHandleBounds.contains(x, y)) {
+            // If enter hovering stashed taskbar, start hint.
+            startStashedTaskbarHover(/* isHovered = */ true);
+        } else if (mBottomEdgeBounds.contains(x, y)) {
+            // If hover screen's bottom edge not below the stashed taskbar, unstash it.
+            mTaskbarActivityContext.onSwipeToUnstashTaskbar();
+        }
+    }
+
+    private void startStashedTaskbarHover(boolean isHovered) {
+        mTaskbarActivityContext.startTaskbarUnstashHint(isHovered, /* forceUnstash = */ true);
+        mIsStashedTaskbarHovered = isHovered;
+    }
 }
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
index 6243471..f5c78f6 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTaskbar.java
@@ -17,6 +17,7 @@
 
 import static androidx.test.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.PERSISTENT;
 import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT;
 
@@ -264,6 +265,39 @@
                 .dragToSplitscreen(TEST_APP_PACKAGE, CALCULATOR_APP_PACKAGE);
     }
 
+    @Test
+    @TaskbarModeSwitch(mode = TRANSIENT)
+    public void testShowTaskbarUnstashHintOnHover() {
+        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_CURSOR_HOVER_STATES, true)) {
+            getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE);
+            mLauncher.getLaunchedAppState().hoverToShowTaskbarUnstashHint();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    @TaskbarModeSwitch(mode = TRANSIENT)
+    public void testUnstashTaskbarOnScreenBottomEdgeHover() {
+        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_CURSOR_HOVER_STATES, true)) {
+            getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE);
+            mLauncher.getLaunchedAppState().hoverScreenBottomEdgeToUnstashTaskbar();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    @TaskbarModeSwitch(mode = TRANSIENT)
+    public void testHoverBelowHintedTaskbarToUnstash() {
+        try (AutoCloseable flag = TestUtil.overrideFlag(ENABLE_CURSOR_HOVER_STATES, true)) {
+            getTaskbar().getAppIcon(TEST_APP_NAME).launch(TEST_APP_PACKAGE);
+            mLauncher.getLaunchedAppState().hoverBelowHintedTaskbarToUnstash();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     private Taskbar getTaskbar() {
         Taskbar taskbar = mLauncher.getLaunchedAppState().getTaskbar();
         List<String> taskbarIconNames = taskbar.getIconNames();
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index b472cdb..601b07e 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -90,6 +90,7 @@
     public static final String REQUEST_DISABLE_TRANSIENT_TASKBAR = "disable-transient-taskbar";
     public static final String REQUEST_UNSTASH_TASKBAR_IF_STASHED = "unstash-taskbar-if-stashed";
     public static final String REQUEST_STASHED_TASKBAR_HEIGHT = "stashed-taskbar-height";
+    public static final String REQUEST_STASHED_TASKBAR_SCALE = "taskbar-stash-handle-scale";
     public static final String REQUEST_RECREATE_TASKBAR = "recreate-taskbar";
     public static final String REQUEST_APP_LIST_FREEZE_FLAGS = "app-list-freeze-flags";
     public static final String REQUEST_APPS_LIST_SCROLL_Y = "apps-list-scroll-y";
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 58d5a36..f52b82d 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -25,13 +25,17 @@
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_ENABLE_MANUAL_TASKBAR_STASHING;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_SHELL_DRAG_READY;
 import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_STASHED_TASKBAR_HEIGHT;
+import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_STASHED_TASKBAR_SCALE;
 
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.SystemClock;
 import android.view.MotionEvent;
+import android.view.ViewConfiguration;
 
 import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.Condition;
+import androidx.test.uiautomator.UiDevice;
 
 import com.android.launcher3.testing.shared.TestProtocol;
 
@@ -43,6 +47,18 @@
     // More drag steps than Launchables to give the window manager time to register the drag.
     private static final int DEFAULT_DRAG_STEPS = 35;
 
+    // UNSTASHED_TASKBAR_HANDLE_HINT_SCALE value from TaskbarStashController.
+    private static final float UNSTASHED_TASKBAR_HANDLE_HINT_SCALE = 1.1f;
+
+    private final Condition<UiDevice, Boolean> mStashedTaskbarHintScaleCondition =
+            device -> mLauncher.getTestInfo(REQUEST_STASHED_TASKBAR_SCALE).getFloat(
+                    TestProtocol.TEST_INFO_RESPONSE_FIELD) - UNSTASHED_TASKBAR_HANDLE_HINT_SCALE
+                    < 0.00001f;
+
+    private final Condition<UiDevice, Boolean> mStashedTaskbarDefaultScaleCondition =
+            device -> mLauncher.getTestInfo(REQUEST_STASHED_TASKBAR_SCALE).getFloat(
+                    TestProtocol.TEST_INFO_RESPONSE_FIELD) - 1f < 0.00001f;
+
     LaunchedAppState(LauncherInstrumentation launcher) {
         super(launcher);
     }
@@ -187,4 +203,86 @@
             }
         }
     }
+
+    /**
+     * Emulate the cursor hovering the screen edge to unstash the taskbar.
+     *
+     * <p>This unstashing occurs when not actively hovering the taskbar.
+     */
+    public void hoverScreenBottomEdgeToUnstashTaskbar() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "cursor hover entering screen edge to unstash taskbar")) {
+            mLauncher.getDevice().wait(mStashedTaskbarDefaultScaleCondition,
+                    ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT);
+
+            long downTime = SystemClock.uptimeMillis();
+            int leftEdge = 10;
+            Point taskbarUnstashArea = new Point(leftEdge, mLauncher.getRealDisplaySize().y - 1);
+            mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
+                    new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
+
+            mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
+        }
+    }
+
+    /**
+     * Emulate the cursor hovering the taskbar to get unstash hint, then hovering below to unstash.
+     */
+    public void hoverBelowHintedTaskbarToUnstash() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "cursor hover entering stashed taskbar")) {
+            long downTime = SystemClock.uptimeMillis();
+            Point stashedTaskbarHintArea = new Point(mLauncher.getRealDisplaySize().x / 2,
+                    mLauncher.getRealDisplaySize().y - 1);
+            mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
+                    new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y), null);
+
+            mLauncher.getDevice().wait(mStashedTaskbarHintScaleCondition,
+                    LauncherInstrumentation.WAIT_TIME_MS);
+
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                         "cursor hover enter below taskbar to unstash")) {
+                downTime = SystemClock.uptimeMillis();
+                Point taskbarUnstashArea = new Point(mLauncher.getRealDisplaySize().x / 2,
+                        mLauncher.getRealDisplaySize().y - 1);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
+                        new Point(taskbarUnstashArea.x, taskbarUnstashArea.y), null);
+
+                mLauncher.waitForSystemLauncherObject(TASKBAR_RES_ID);
+            }
+        }
+    }
+
+    /**
+     * Emulate the cursor entering and exiting a hover over the taskbar.
+     */
+    public void hoverToShowTaskbarUnstashHint() {
+        try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+             LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+                     "cursor hover entering stashed taskbar")) {
+            long downTime = SystemClock.uptimeMillis();
+            Point stashedTaskbarHintArea = new Point(mLauncher.getRealDisplaySize().x / 2,
+                    mLauncher.getRealDisplaySize().y - 1);
+            mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_ENTER,
+                    new Point(stashedTaskbarHintArea.x, stashedTaskbarHintArea.y), null);
+
+            mLauncher.getDevice().wait(mStashedTaskbarHintScaleCondition,
+                    LauncherInstrumentation.WAIT_TIME_MS);
+
+            try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
+                         "cursor hover exiting stashed taskbar")) {
+                Point outsideStashedTaskbarHintArea = new Point(
+                        mLauncher.getRealDisplaySize().x / 2,
+                        mLauncher.getRealDisplaySize().y - 500);
+                mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_HOVER_EXIT,
+                        new Point(outsideStashedTaskbarHintArea.x, outsideStashedTaskbarHintArea.y),
+                        null);
+
+                mLauncher.getDevice().wait(mStashedTaskbarDefaultScaleCondition,
+                        LauncherInstrumentation.WAIT_TIME_MS);
+            }
+        }
+    }
 }