[Drag To Hide] Shows notification when user drag-to-hide a11y floating button

* Click on notification to bring back a11y floating button
* Swipe notification to remove a11y floating button and its services
* A11y service key changed to dismiss notification
* Align to create ACCESSIBILITY channel in systemui NotificationChannels

Bug: 298718415
Flag: ACONFIG com.android.systemui.floating_menu_drag_to_hide DEVELOPMENT
Test: atest MenuViewLayerTest MenuNotificationFactoryTest NotificationChannelsTest

Change-Id: Ie209ec946ea15903acfa6913492605277e9d7057
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index 21263a9..f7b1a26 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -10,6 +10,13 @@
 }
 
 flag {
+    name: "floating_menu_drag_to_hide"
+    namespace: "accessibility"
+    description: "Allows users to hide the FAB then use notification to dismiss or bring it back."
+    bug: "298718415"
+}
+
+flag {
     name: "floating_menu_ime_displacement_animation"
     namespace: "accessibility"
     description: "Adds an animation for when the FAB is displaced by an IME becoming visible."
@@ -28,4 +35,4 @@
     namespace: "accessibility"
     description: "Animates the floating menu's transition between curved and jagged edges."
     bug: "281140482"
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index f4b25a7..8d560c3 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2274,6 +2274,8 @@
     <string name="notification_channel_storage">Storage</string>
     <!-- Title for the notification channel for hints and suggestions. [CHAR LIMIT=NONE] -->
     <string name="notification_channel_hints">Hints</string>
+    <!-- Title for the notification channel for accessibility related (i.e. accessibility floating menu). [CHAR LIMIT=NONE] -->
+    <string name="notification_channel_accessibility">Accessibility</string>
 
     <!-- App label of the instant apps notification [CHAR LIMIT=60] -->
     <string name="instant_apps">Instant Apps</string>
@@ -2544,6 +2546,11 @@
     <string name="accessibility_floating_button_docking_tooltip">Move button to the edge to hide it temporarily</string>
     <!-- Text for the undo action button of the message view of the accessibility floating menu to perform undo operation. [CHAR LIMIT=30]-->
     <string name="accessibility_floating_button_undo">Undo</string>
+    <!-- Notification title shown when accessibility floating button is in hidden state. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_floating_button_hidden_notification_title">Accessibility button hidden</string>
+    <!-- Notification content text to explain user can tap notification to bring back accessibility floating button. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_floating_button_hidden_notification_text">Tap to show accessibility button</string>
+
     <!-- Text for the message view with undo action of the accessibility floating menu to show which feature shortcut was removed. [CHAR LIMIT=30]-->
     <string name="accessibility_floating_button_undo_message_label_text"><xliff:g id="feature name" example="Magnification">%s</xliff:g> shortcut removed</string>
     <!-- Text for the message view with undo action of the accessibility floating menu to show how many features shortcuts were removed. [CHAR LIMIT=30]-->
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java
index 49e0df6..568b24d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java
@@ -24,6 +24,7 @@
 import androidx.annotation.NonNull;
 import androidx.dynamicanimation.animation.DynamicAnimation;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.wm.shell.common.bubbles.DismissView;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 
@@ -116,6 +117,11 @@
         mMagnetizedObject.setMagnetListener(magnetListener);
     }
 
+    @VisibleForTesting
+    MagnetizedObject.MagnetListener getMagnetListener() {
+        return mMagnetizedObject.getMagnetListener();
+    }
+
     void maybeConsumeDownMotionEvent(MotionEvent event) {
         mMagnetizedObject.maybeConsumeMotionEvent(event);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactory.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactory.java
new file mode 100644
index 0000000..b5eeaa1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactory.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 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.systemui.accessibility.floatingmenu;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import com.android.systemui.res.R;
+import com.android.systemui.util.NotificationChannels;
+
+class MenuNotificationFactory {
+    public static final String ACTION_UNDO =
+            "com.android.systemui.accessibility.floatingmenu.action.UNDO";
+    public static final String ACTION_DELETE =
+            "com.android.systemui.accessibility.floatingmenu.action.DELETE";
+
+    private final Context mContext;
+
+    MenuNotificationFactory(Context context) {
+        mContext = context;
+    }
+
+    public Notification createHiddenNotification() {
+        final CharSequence title = mContext.getText(
+                R.string.accessibility_floating_button_hidden_notification_title);
+        final CharSequence content = mContext.getText(
+                R.string.accessibility_floating_button_hidden_notification_text);
+
+        return new Notification.Builder(mContext, NotificationChannels.ALERTS)
+                .setContentTitle(title)
+                .setContentText(content)
+                .setSmallIcon(R.drawable.ic_settings_24dp)
+                .setContentIntent(buildUndoIntent())
+                .setDeleteIntent(buildDeleteIntent())
+                .setColor(mContext.getResources().getColor(
+                        com.android.internal.R.color.system_notification_accent_color))
+                .setLocalOnly(true)
+                .setCategory(Notification.CATEGORY_SYSTEM)
+                .build();
+    }
+
+    private PendingIntent buildUndoIntent() {
+        final Intent intent = new Intent(ACTION_UNDO);
+
+        return PendingIntent.getBroadcast(mContext, /* requestCode= */ 0, intent,
+                PendingIntent.FLAG_IMMUTABLE);
+
+    }
+
+    private PendingIntent buildDeleteIntent() {
+        final Intent intent = new Intent(ACTION_DELETE);
+
+        return PendingIntent.getBroadcastAsUser(mContext, /* requestCode= */ 0, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT
+                        | PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT);
+
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index 62d5feb..6869bba 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -26,16 +26,21 @@
 import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType;
 import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState;
 import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.Index;
+import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_DELETE;
+import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_UNDO;
 import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
 import android.annotation.IntDef;
 import android.annotation.StringDef;
 import android.annotation.SuppressLint;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
 import android.content.ComponentCallbacks;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
@@ -58,6 +63,7 @@
 
 import com.android.internal.accessibility.dialog.AccessibilityTarget;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.internal.util.Preconditions;
 import com.android.systemui.Flags;
 import com.android.systemui.res.R;
@@ -91,6 +97,8 @@
     private final MenuViewAppearance mMenuViewAppearance;
     private final MenuAnimationController mMenuAnimationController;
     private final AccessibilityManager mAccessibilityManager;
+    private final NotificationManager mNotificationManager;
+    private final MenuNotificationFactory mNotificationFactory;
     private final Handler mHandler = new Handler(Looper.getMainLooper());
     private final IAccessibilityFloatingMenu mFloatingMenu;
     private final SecureSettings mSecureSettings;
@@ -103,7 +111,9 @@
     private final Rect mImeInsetsRect = new Rect();
     private boolean mIsMigrationTooltipShowing;
     private boolean mShouldShowDockTooltip;
+    private boolean mIsNotificationShown;
     private Optional<MenuEduTooltipView> mEduTooltipView = Optional.empty();
+    private BroadcastReceiver mNotificationActionReceiver;
 
     @IntDef({
             LayerIndex.MENU_VIEW,
@@ -184,10 +194,16 @@
         mMenuViewAppearance = new MenuViewAppearance(context, windowManager);
         mMenuView = new MenuView(context, mMenuViewModel, mMenuViewAppearance);
         mMenuAnimationController = mMenuView.getMenuAnimationController();
-        mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);
+        if (Flags.floatingMenuDragToHide()) {
+            mMenuAnimationController.setDismissCallback(this::hideMenuAndShowNotification);
+        } else {
+            mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);
+        }
         mMenuAnimationController.setSpringAnimationsEndAction(this::onSpringAnimationsEndAction);
         mDismissView = new DismissView(context);
         DismissViewUtils.setup(mDismissView);
+        mNotificationFactory = new MenuNotificationFactory(context);
+        mNotificationManager = context.getSystemService(NotificationManager.class);
         mDragToInteractAnimationController = new DragToInteractAnimationController(
                 mDismissView, mMenuView);
         mDragToInteractAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() {
@@ -204,7 +220,11 @@
 
             @Override
             public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
-                hideMenuAndShowMessage();
+                if (Flags.floatingMenuDragToHide()) {
+                    hideMenuAndShowNotification();
+                } else {
+                    hideMenuAndShowMessage();
+                }
                 mDismissView.hide();
                 mDragToInteractAnimationController.animateDismissMenu(/* scaleUp= */ false);
             }
@@ -218,18 +238,25 @@
         mMessageView = new MenuMessageView(context);
 
         mMenuView.setOnTargetFeaturesChangeListener(newTargetFeatures -> {
-            if (newTargetFeatures.size() < 1) {
-                return;
-            }
-
-            // During the undo action period, the pending action will be canceled and undo back
-            // to the previous state if users did any action related to the accessibility features.
-            if (mMessageView.getVisibility() == VISIBLE) {
+            if (Flags.floatingMenuDragToHide()) {
+                dismissNotification();
                 undo();
-            }
+            } else {
+                if (newTargetFeatures.size() < 1) {
+                    return;
+                }
 
-            final TextView messageText = (TextView) mMessageView.getChildAt(Index.TEXT_VIEW);
-            messageText.setText(getMessageText(newTargetFeatures));
+                // During the undo action period, the pending action will be canceled and undo back
+                // to the previous state if users did any action related to the accessibility
+                // features.
+                if (mMessageView.getVisibility() == VISIBLE) {
+                    undo();
+                }
+
+
+                final TextView messageText = (TextView) mMessageView.getChildAt(Index.TEXT_VIEW);
+                messageText.setText(getMessageText(newTargetFeatures));
+            }
         });
 
         addView(mMenuView, LayerIndex.MENU_VIEW);
@@ -456,6 +483,50 @@
         mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE));
     }
 
+    private void hideMenuAndShowNotification() {
+        mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE));
+        showNotification();
+    }
+
+    private void showNotification() {
+        registerReceiverIfNeeded();
+        if (!mIsNotificationShown) {
+            mNotificationManager.notify(
+                    SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN,
+                    mNotificationFactory.createHiddenNotification());
+            mIsNotificationShown = true;
+        }
+    }
+
+    private void dismissNotification() {
+        unregisterReceiverIfNeeded();
+        if (mIsNotificationShown) {
+            mNotificationManager.cancel(
+                    SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN);
+            mIsNotificationShown = false;
+        }
+    }
+
+    private void registerReceiverIfNeeded() {
+        if (mNotificationActionReceiver != null) {
+            return;
+        }
+        mNotificationActionReceiver = new MenuNotificationActionReceiver();
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(ACTION_UNDO);
+        intentFilter.addAction(ACTION_DELETE);
+        getContext().registerReceiver(mNotificationActionReceiver, intentFilter,
+                Context.RECEIVER_EXPORTED);
+    }
+
+    private void unregisterReceiverIfNeeded() {
+        if (mNotificationActionReceiver == null) {
+            return;
+        }
+        getContext().unregisterReceiver(mNotificationActionReceiver);
+        mNotificationActionReceiver = null;
+    }
+
     private void undo() {
         mHandler.removeCallbacksAndMessages(/* token= */ null);
         mMessageView.setVisibility(GONE);
@@ -464,4 +535,23 @@
         mMenuView.setVisibility(VISIBLE);
         mMenuAnimationController.startGrowAnimation();
     }
+
+    @VisibleForTesting
+    DragToInteractAnimationController getDragToInteractAnimationController() {
+        return mDragToInteractAnimationController;
+    }
+
+    private class MenuNotificationActionReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (ACTION_UNDO.equals(action)) {
+                dismissNotification();
+                undo();
+            } else if (ACTION_DELETE.equals(action)) {
+                dismissNotification();
+                mDismissMenuAction.run();
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactoryTest.java
new file mode 100644
index 0000000..9dd337e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuNotificationFactoryTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 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.systemui.accessibility.floatingmenu;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+public class MenuNotificationFactoryTest extends SysuiTestCase {
+    private MenuNotificationFactory mMenuNotificationFactory;
+
+    @Before
+    public void setUp() {
+        mMenuNotificationFactory = new MenuNotificationFactory(mContext);
+    }
+
+    @Test
+    public void createHiddenNotification_hasUndoAndDeleteAction() {
+        Notification notification = mMenuNotificationFactory.createHiddenNotification();
+
+        assertThat(notification.contentIntent.getIntent().getAction()).isEqualTo(
+                MenuNotificationFactory.ACTION_UNDO);
+        assertThat(notification.deleteIntent.getIntent().getAction()).isEqualTo(
+                MenuNotificationFactory.ACTION_DELETE);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index be6f3ff..68879a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -21,15 +21,30 @@
 import static android.view.WindowInsets.Type.displayCutout;
 import static android.view.WindowInsets.Type.ime;
 import static android.view.WindowInsets.Type.systemBars;
+
+import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_DELETE;
+import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_UNDO;
 import static com.android.systemui.accessibility.floatingmenu.MenuViewLayer.LayerIndex;
+
 import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
@@ -40,6 +55,8 @@
 import android.os.UserHandle;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
@@ -53,16 +70,21 @@
 import androidx.dynamicanimation.animation.SpringAnimation;
 import androidx.test.filters.SmallTest;
 
+import com.android.internal.messages.nano.SystemMessageProto;
 import com.android.systemui.Flags;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.SysuiTestableContext;
 import com.android.systemui.util.settings.SecureSettings;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 
 import org.junit.After;
 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.Spy;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
@@ -98,6 +120,12 @@
     @Rule
     public MockitoRule mockito = MockitoJUnit.rule();
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Spy
+    private SysuiTestableContext mSpyContext = getContext();
     @Mock
     private IAccessibilityFloatingMenu mFloatingMenu;
 
@@ -110,8 +138,12 @@
     @Mock
     private AccessibilityManager mStubAccessibilityManager;
 
+    private final NotificationManager mMockNotificationManager = mock(NotificationManager.class);
+
     @Before
     public void setUp() throws Exception {
+        mSpyContext.addMockSystemService(Context.NOTIFICATION_SERVICE, mMockNotificationManager);
+
         final Rect mDisplayBounds = new Rect();
         mDisplayBounds.set(/* left= */ 0, /* top= */ 0, DISPLAY_WINDOW_WIDTH,
                 DISPLAY_WINDOW_HEIGHT);
@@ -119,31 +151,31 @@
                 new WindowMetrics(mDisplayBounds, fakeDisplayInsets(), /* density = */ 0.0f));
         doReturn(mWindowMetrics).when(mStubWindowManager).getCurrentWindowMetrics();
 
-        mMenuViewLayer = new MenuViewLayer(mContext, mStubWindowManager, mStubAccessibilityManager,
-                mFloatingMenu, mSecureSettings);
+        mMenuViewLayer = new MenuViewLayer(mSpyContext, mStubWindowManager,
+                mStubAccessibilityManager, mFloatingMenu, mSecureSettings);
         mMenuView = (MenuView) mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW);
         mMenuAnimationController = mMenuView.getMenuAnimationController();
 
         mLastAccessibilityButtonTargets =
-                Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                Settings.Secure.getStringForUser(mSpyContext.getContentResolver(),
                         Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT);
         mLastEnabledAccessibilityServices =
-                Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                Settings.Secure.getStringForUser(mSpyContext.getContentResolver(),
                         Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, UserHandle.USER_CURRENT);
 
         mMenuViewLayer.onAttachedToWindow();
-        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+        Settings.Secure.putStringForUser(mSpyContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", UserHandle.USER_CURRENT);
-        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+        Settings.Secure.putStringForUser(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "", UserHandle.USER_CURRENT);
     }
 
     @After
     public void tearDown() throws Exception {
-        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+        Settings.Secure.putStringForUser(mSpyContext.getContentResolver(),
                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, mLastAccessibilityButtonTargets,
                 UserHandle.USER_CURRENT);
-        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+        Settings.Secure.putStringForUser(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, mLastEnabledAccessibilityServices,
                 UserHandle.USER_CURRENT);
 
@@ -188,7 +220,7 @@
         setupEnabledAccessibilityServiceList();
 
         mMenuViewLayer.mDismissMenuAction.run();
-        final String value = Settings.Secure.getString(mContext.getContentResolver(),
+        final String value = Settings.Secure.getString(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
 
         assertThat(value).isEqualTo("");
@@ -203,7 +235,7 @@
                 AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY)).thenReturn(stubShortcutTargets);
 
         mMenuViewLayer.mDismissMenuAction.run();
-        final String value = Settings.Secure.getString(mContext.getContentResolver(),
+        final String value = Settings.Secure.getString(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
 
         assertThat(value).isEqualTo(TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString());
@@ -278,9 +310,60 @@
         assertThat(mMenuView.getTranslationX()).isEqualTo(beforePosition.x);
         assertThat(mMenuView.getTranslationY()).isEqualTo(beforePosition.y);
     }
+    @Test
+    @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE)
+    public void onReleasedInTarget_hideMenuAndShowNotificationWithExpectedActions() {
+        dragMenuThenReleasedInTarget();
+
+        verify(mMockNotificationManager).notify(
+                eq(SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN),
+                any(Notification.class));
+        ArgumentCaptor<IntentFilter> intentFilterCaptor = ArgumentCaptor.forClass(
+                IntentFilter.class);
+        verify(mSpyContext).registerReceiver(
+                any(BroadcastReceiver.class),
+                intentFilterCaptor.capture(),
+                anyInt());
+        assertThat(intentFilterCaptor.getValue().matchAction(ACTION_UNDO)).isTrue();
+        assertThat(intentFilterCaptor.getValue().matchAction(ACTION_DELETE)).isTrue();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE)
+    public void receiveActionUndo_dismissNotificationAndMenuVisible() {
+        ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = ArgumentCaptor.forClass(
+                BroadcastReceiver.class);
+        dragMenuThenReleasedInTarget();
+
+        verify(mSpyContext).registerReceiver(broadcastReceiverCaptor.capture(),
+                any(IntentFilter.class), anyInt());
+        broadcastReceiverCaptor.getValue().onReceive(mSpyContext, new Intent(ACTION_UNDO));
+
+        verify(mSpyContext).unregisterReceiver(broadcastReceiverCaptor.getValue());
+        verify(mMockNotificationManager).cancel(
+                SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN);
+        assertThat(mMenuView.getVisibility()).isEqualTo(VISIBLE);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE)
+    public void receiveActionDelete_dismissNotificationAndHideMenu() {
+        ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = ArgumentCaptor.forClass(
+                BroadcastReceiver.class);
+        dragMenuThenReleasedInTarget();
+
+        verify(mSpyContext).registerReceiver(broadcastReceiverCaptor.capture(),
+                any(IntentFilter.class), anyInt());
+        broadcastReceiverCaptor.getValue().onReceive(mSpyContext, new Intent(ACTION_DELETE));
+
+        verify(mSpyContext).unregisterReceiver(broadcastReceiverCaptor.getValue());
+        verify(mMockNotificationManager).cancel(
+                SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN);
+        verify(mFloatingMenu).hide();
+    }
 
     private void setupEnabledAccessibilityServiceList() {
-        Settings.Secure.putString(mContext.getContentResolver(),
+        Settings.Secure.putString(mSpyContext.getContentResolver(),
                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
                 TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString());
 
@@ -344,6 +427,12 @@
                     springAnimation.skipToEnd();
                     springAnimation.doAnimationFrame(500);
                 });
+    }
 
+    private void dragMenuThenReleasedInTarget() {
+        MagnetizedObject.MagnetListener magnetListener =
+                mMenuViewLayer.getDragToInteractAnimationController().getMagnetListener();
+        magnetListener.onReleasedInTarget(
+                new MagnetizedObject.MagneticTarget(mock(View.class), 200));
     }
 }
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index b403a7f..7f542d1 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -408,5 +408,9 @@
     // Notify the user about external display events related to screenshot.
     // Package: com.android.systemui
     NOTE_GLOBAL_SCREENSHOT_EXTERNAL_DISPLAY = 1008;
+
+    // Notify the user that accessibility floating menu is hidden.
+    // Package: com.android.systemui
+    NOTE_A11Y_FLOATING_MENU_HIDDEN = 1009;
   }
 }