• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.accessibility.floatingmenu;
18 
19 import static android.view.WindowInsets.Type.ime;
20 
21 import static androidx.core.view.WindowInsetsCompat.Type;
22 
23 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_BUTTON_COMPONENT_NAME;
24 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
25 import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.Index;
26 import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_DELETE;
27 import static com.android.systemui.accessibility.floatingmenu.MenuNotificationFactory.ACTION_UNDO;
28 import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat;
29 
30 import android.annotation.IntDef;
31 import android.annotation.StringDef;
32 import android.annotation.SuppressLint;
33 import android.app.NotificationManager;
34 import android.app.StatusBarManager;
35 import android.content.BroadcastReceiver;
36 import android.content.ComponentCallbacks;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.content.IntentFilter;
40 import android.content.pm.PackageManager;
41 import android.content.pm.ResolveInfo;
42 import android.content.res.Configuration;
43 import android.content.res.Resources;
44 import android.graphics.Rect;
45 import android.os.Bundle;
46 import android.os.Handler;
47 import android.os.Looper;
48 import android.os.UserHandle;
49 import android.provider.Settings;
50 import android.provider.SettingsStringUtil;
51 import android.util.ArraySet;
52 import android.view.MotionEvent;
53 import android.view.View;
54 import android.view.ViewTreeObserver;
55 import android.view.WindowInsets;
56 import android.view.WindowManager;
57 import android.view.WindowMetrics;
58 import android.view.accessibility.AccessibilityManager;
59 import android.widget.FrameLayout;
60 import android.widget.TextView;
61 
62 import androidx.annotation.NonNull;
63 import androidx.core.view.AccessibilityDelegateCompat;
64 import androidx.lifecycle.Observer;
65 import androidx.recyclerview.widget.RecyclerView;
66 import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
67 
68 import com.android.internal.accessibility.common.ShortcutConstants;
69 import com.android.internal.accessibility.dialog.AccessibilityTarget;
70 import com.android.internal.annotations.VisibleForTesting;
71 import com.android.internal.messages.nano.SystemMessageProto;
72 import com.android.internal.util.Preconditions;
73 import com.android.systemui.Flags;
74 import com.android.systemui.navigationbar.NavigationModeController;
75 import com.android.systemui.res.R;
76 import com.android.systemui.util.settings.SecureSettings;
77 import com.android.wm.shell.bubbles.DismissViewUtils;
78 import com.android.wm.shell.shared.bubbles.DismissView;
79 import com.android.wm.shell.shared.magnetictarget.MagnetizedObject;
80 
81 import java.lang.annotation.Retention;
82 import java.lang.annotation.RetentionPolicy;
83 import java.util.List;
84 import java.util.Optional;
85 
86 /**
87  * The basic interactions with the child views {@link MenuView}, {@link DismissView}, and
88  * {@link MenuMessageView}. When dragging the menu view, the dismissed view would be shown at the
89  * same time. If the menu view overlaps on the dismissed circle view and drops out, the menu
90  * message view would be shown and allowed users to undo it.
91  */
92 @SuppressLint("ViewConstructor")
93 class MenuViewLayer extends FrameLayout implements
94         ViewTreeObserver.OnComputeInternalInsetsListener, View.OnClickListener, ComponentCallbacks,
95         MenuView.OnMoveToTuckedListener {
96     private static final int SHOW_MESSAGE_DELAY_MS = 3000;
97 
98     /**
99      * Counter indicating the FAB was dragged to the Dismiss action button.
100      *
101      * <p>Defined in frameworks/proto_logging/stats/express/catalog/accessibility.cfg.
102      */
103     static final String TEX_METRIC_DISMISS = "accessibility.value_fab_shortcut_dismiss";
104 
105     /**
106      * Counter indicating the FAB was dragged to the Edit action button.
107      *
108      * <p>Defined in frameworks/proto_logging/stats/express/catalog/accessibility.cfg.
109      */
110     static final String TEX_METRIC_EDIT = "accessibility.value_fab_shortcut_edit";
111 
112     private final WindowManager mWindowManager;
113     private final MenuView mMenuView;
114     private final MenuListViewTouchHandler mMenuListViewTouchHandler;
115     private final MenuMessageView mMessageView;
116     private final DismissView mDismissView;
117     private final DragToInteractView mDragToInteractView;
118 
119     private final MenuViewAppearance mMenuViewAppearance;
120     private final MenuAnimationController mMenuAnimationController;
121     private final AccessibilityManager mAccessibilityManager;
122     private final NotificationManager mNotificationManager;
123     private StatusBarManager mStatusBarManager;
124     private final MenuNotificationFactory mNotificationFactory;
125     private final Handler mHandler = new Handler(Looper.getMainLooper());
126     private final IAccessibilityFloatingMenu mFloatingMenu;
127     private final SecureSettings mSecureSettings;
128     private final DragToInteractAnimationController mDragToInteractAnimationController;
129     private final MenuViewModel mMenuViewModel;
130     private final Observer<Boolean> mDockTooltipObserver =
131             this::onDockTooltipVisibilityChanged;
132     private final Observer<Boolean> mMigrationTooltipObserver =
133             this::onMigrationTooltipVisibilityChanged;
134     private final Rect mImeInsetsRect = new Rect();
135     private boolean mIsMigrationTooltipShowing;
136     private boolean mShouldShowDockTooltip;
137     private boolean mIsNotificationShown;
138     private Optional<MenuEduTooltipView> mEduTooltipView = Optional.empty();
139     private BroadcastReceiver mNotificationActionReceiver;
140     private NavigationModeController mNavigationModeController;
141     private NavigationModeController.ModeChangedListener mNavigationModeChangedListender;
142 
143     @IntDef({
144             LayerIndex.MENU_VIEW,
145             LayerIndex.DISMISS_VIEW,
146             LayerIndex.MESSAGE_VIEW,
147             LayerIndex.TOOLTIP_VIEW,
148     })
149     @Retention(RetentionPolicy.SOURCE)
150     @interface LayerIndex {
151         int MENU_VIEW = 0;
152         int DISMISS_VIEW = 1;
153         int MESSAGE_VIEW = 2;
154         int TOOLTIP_VIEW = 3;
155     }
156 
157     @StringDef({
158             TooltipType.MIGRATION,
159             TooltipType.DOCK,
160     })
161     @Retention(RetentionPolicy.SOURCE)
162     @interface TooltipType {
163         String MIGRATION = "migration";
164         String DOCK = "dock";
165     }
166 
167     @VisibleForTesting
168     final Runnable mDismissMenuAction = new Runnable() {
169         @Override
170         public void run() {
171             mAccessibilityManager.enableShortcutsForTargets(
172                     /* enable= */ false,
173                     ShortcutConstants.UserShortcutType.SOFTWARE,
174                     new ArraySet<>(
175                             mAccessibilityManager.getAccessibilityShortcutTargets(SOFTWARE)),
176                     mSecureSettings.getRealUserHandle(UserHandle.USER_CURRENT)
177             );
178             mFloatingMenu.hide();
179         }
180     };
181 
MenuViewLayer(@onNull Context context, WindowManager windowManager, AccessibilityManager accessibilityManager, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance, MenuView menuView, IAccessibilityFloatingMenu floatingMenu, SecureSettings secureSettings, NavigationModeController navigationModeController)182     MenuViewLayer(@NonNull Context context, WindowManager windowManager,
183             AccessibilityManager accessibilityManager,
184             MenuViewModel menuViewModel,
185             MenuViewAppearance menuViewAppearance, MenuView menuView,
186             IAccessibilityFloatingMenu floatingMenu,
187             SecureSettings secureSettings,
188             NavigationModeController navigationModeController) {
189         super(context);
190 
191         // Simplifies the translation positioning and animations
192         setLayoutDirection(LAYOUT_DIRECTION_LTR);
193 
194         mWindowManager = windowManager;
195         mAccessibilityManager = accessibilityManager;
196         mFloatingMenu = floatingMenu;
197         mSecureSettings = secureSettings;
198 
199         mMenuViewModel = menuViewModel;
200         mMenuViewAppearance = menuViewAppearance;
201         mMenuView = menuView;
202         RecyclerView targetFeaturesView = mMenuView.getTargetFeaturesView();
203         targetFeaturesView.setAccessibilityDelegateCompat(
204                 new RecyclerViewAccessibilityDelegate(targetFeaturesView) {
205                     @NonNull
206                     @Override
207                     public AccessibilityDelegateCompat getItemDelegate() {
208                         return new MenuItemAccessibilityDelegate(/* recyclerViewDelegate= */ this,
209                                 mMenuAnimationController, MenuViewLayer.this);
210                     }
211                 });
212         mMenuAnimationController = mMenuView.getMenuAnimationController();
213         mMenuAnimationController.setSpringAnimationsEndAction(this::onSpringAnimationsEndAction);
214         mDismissView = new DismissView(context);
215         mDragToInteractView = new DragToInteractView(context, windowManager);
216         DismissViewUtils.setup(mDismissView);
217         mDismissView.getCircle().setId(R.id.action_remove_menu);
218         mNotificationFactory = new MenuNotificationFactory(context);
219         mNotificationManager = context.getSystemService(NotificationManager.class);
220         mStatusBarManager = context.getSystemService(StatusBarManager.class);
221         mNavigationModeController = navigationModeController;
222         mNavigationModeChangedListender = (mode -> mMenuView.onPositionChanged());
223 
224         if (Flags.floatingMenuDragToEdit()) {
225             mDragToInteractAnimationController = new DragToInteractAnimationController(
226                     mDragToInteractView, mMenuView);
227         } else {
228             mDragToInteractAnimationController = new DragToInteractAnimationController(
229                     mDismissView, mMenuView);
230         }
231         mDragToInteractAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() {
232             @Override
233             public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target,
234                     @NonNull MagnetizedObject<?> draggedObject) {
235                 mDragToInteractAnimationController.animateInteractMenu(
236                         target.getTargetView().getId(), /* scaleUp= */ true);
237             }
238 
239             @Override
240             public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
241                     @NonNull MagnetizedObject<?> draggedObject,
242                     float velocityX, float velocityY, boolean wasFlungOut) {
243                 mDragToInteractAnimationController.animateInteractMenu(
244                         target.getTargetView().getId(), /* scaleUp= */ false);
245             }
246 
247             @Override
248             public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target,
249                     @NonNull MagnetizedObject<?> draggedObject) {
250                 dispatchAccessibilityAction(target.getTargetView().getId());
251             }
252         });
253 
254         mMenuListViewTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController,
255                 mDragToInteractAnimationController);
256         mMenuView.addOnItemTouchListenerToList(mMenuListViewTouchHandler);
257         mMenuView.setMoveToTuckedListener(this);
258 
259         mMessageView = new MenuMessageView(context);
260 
261         mMenuView.setOnTargetFeaturesChangeListener(newTargetFeatures -> {
262             if (Flags.floatingMenuDragToHide()) {
263                 dismissNotification();
264                 if (newTargetFeatures.size() > 0) {
265                     undo();
266                 }
267             } else {
268                 if (newTargetFeatures.size() < 1) {
269                     return;
270                 }
271 
272                 // During the undo action period, the pending action will be canceled and undo back
273                 // to the previous state if users did any action related to the accessibility
274                 // features.
275                 if (mMessageView.getVisibility() == VISIBLE) {
276                     undo();
277                 }
278 
279 
280                 final TextView messageText = (TextView) mMessageView.getChildAt(Index.TEXT_VIEW);
281                 messageText.setText(getMessageText(newTargetFeatures));
282             }
283         });
284 
285         addView(mMenuView, LayerIndex.MENU_VIEW);
286         if (Flags.floatingMenuDragToEdit()) {
287             addView(mDragToInteractView, LayerIndex.DISMISS_VIEW);
288         } else {
289             addView(mDismissView, LayerIndex.DISMISS_VIEW);
290         }
291         addView(mMessageView, LayerIndex.MESSAGE_VIEW);
292 
293         setClipChildren(true);
294 
295         setClickable(false);
296         setFocusable(false);
297         setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
298     }
299 
300     @Override
onConfigurationChanged(@onNull Configuration newConfig)301     public void onConfigurationChanged(@NonNull Configuration newConfig) {
302         mDragToInteractView.updateResources();
303         mDismissView.updateResources();
304         mDragToInteractAnimationController.updateResources();
305         mMenuAnimationController.skipAnimations();
306     }
307 
308     @Override
onLowMemory()309     public void onLowMemory() {
310         // Do nothing.
311     }
312 
getMessageText(List<AccessibilityTarget> newTargetFeatures)313     private String getMessageText(List<AccessibilityTarget> newTargetFeatures) {
314         Preconditions.checkArgument(newTargetFeatures.size() > 0,
315                 "The list should at least have one feature.");
316 
317         final int featuresSize = newTargetFeatures.size();
318         final Resources resources = getResources();
319         if (featuresSize == 1) {
320             return resources.getString(
321                     R.string.accessibility_floating_button_undo_message_label_text,
322                     newTargetFeatures.get(0).getLabel());
323         }
324 
325         return icuMessageFormat(resources,
326                 R.string.accessibility_floating_button_undo_message_number_text, featuresSize);
327     }
328 
329     @Override
onInterceptTouchEvent(MotionEvent event)330     public boolean onInterceptTouchEvent(MotionEvent event) {
331         if (mMenuView.maybeMoveOutEdgeAndShow((int) event.getX(), (int) event.getY())) {
332             return true;
333         }
334 
335         return super.onInterceptTouchEvent(event);
336     }
337 
338     @Override
onAttachedToWindow()339     protected void onAttachedToWindow() {
340         super.onAttachedToWindow();
341 
342         mMenuView.show();
343         setOnClickListener(this);
344         setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets));
345         getViewTreeObserver().addOnComputeInternalInsetsListener(this);
346         mMenuViewModel.getDockTooltipVisibilityData().observeForever(mDockTooltipObserver);
347         mMenuViewModel.getMigrationTooltipVisibilityData().observeForever(
348                 mMigrationTooltipObserver);
349         mMessageView.setUndoListener(view -> undo());
350         getContext().registerComponentCallbacks(this);
351         mNavigationModeController.addListener(mNavigationModeChangedListender);
352     }
353 
354     @Override
onDetachedFromWindow()355     protected void onDetachedFromWindow() {
356         super.onDetachedFromWindow();
357 
358         mMenuView.hide();
359         setOnClickListener(null);
360         setOnApplyWindowInsetsListener(null);
361         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
362         mMenuViewModel.getDockTooltipVisibilityData().removeObserver(mDockTooltipObserver);
363         mMenuViewModel.getMigrationTooltipVisibilityData().removeObserver(
364                 mMigrationTooltipObserver);
365         mHandler.removeCallbacksAndMessages(/* token= */ null);
366         getContext().unregisterComponentCallbacks(this);
367         mNavigationModeController.removeListener(mNavigationModeChangedListender);
368     }
369 
370     @Override
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)371     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
372         inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
373 
374         if (mEduTooltipView.isPresent()) {
375             final int x = (int) getX();
376             final int y = (int) getY();
377             inoutInfo.touchableRegion.union(new Rect(x, y, x + getWidth(), y + getHeight()));
378         }
379     }
380 
381     @Override
onClick(View v)382     public void onClick(View v) {
383         mEduTooltipView.ifPresent(this::removeTooltip);
384     }
385 
onWindowInsetsApplied(WindowInsets insets)386     private WindowInsets onWindowInsetsApplied(WindowInsets insets) {
387         final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics();
388         final WindowInsets windowInsets = windowMetrics.getWindowInsets();
389         final Rect imeInsetsRect = windowInsets.getInsets(ime()).toRect();
390         if (!imeInsetsRect.equals(mImeInsetsRect)) {
391             final Rect windowBounds = new Rect(windowMetrics.getBounds());
392             final Rect systemBarsAndDisplayCutoutInsetsRect =
393                     windowInsets.getInsetsIgnoringVisibility(
394                             Type.systemBars() | Type.displayCutout()).toRect();
395             final float imeTop =
396                     windowBounds.height() - systemBarsAndDisplayCutoutInsetsRect.top
397                             - imeInsetsRect.bottom;
398 
399             mMenuViewAppearance.onImeVisibilityChanged(windowInsets.isVisible(ime()), imeTop);
400 
401             mMenuView.onEdgeChanged();
402             mMenuView.onPositionChanged(/* animateMovement = */ true);
403 
404             mImeInsetsRect.set(imeInsetsRect);
405         }
406 
407         return insets;
408     }
409 
onMigrationTooltipVisibilityChanged(boolean visible)410     private void onMigrationTooltipVisibilityChanged(boolean visible) {
411         mIsMigrationTooltipShowing = visible;
412 
413         if (mIsMigrationTooltipShowing) {
414             mEduTooltipView = Optional.of(new MenuEduTooltipView(mContext, mMenuViewAppearance));
415             mEduTooltipView.ifPresent(
416                     view -> addTooltipView(view, getMigrationMessage(), TooltipType.MIGRATION));
417         }
418     }
419 
onDockTooltipVisibilityChanged(boolean hasSeenTooltip)420     private void onDockTooltipVisibilityChanged(boolean hasSeenTooltip) {
421         mShouldShowDockTooltip = !hasSeenTooltip;
422     }
423 
onMoveToTuckedChanged(boolean moveToTuck)424     public void onMoveToTuckedChanged(boolean moveToTuck) {
425         if (moveToTuck) {
426             final Rect bounds = mMenuViewAppearance.getWindowAvailableBounds();
427             final int[] location = getLocationOnScreen();
428             bounds.offset(
429                     location[0],
430                     location[1]
431             );
432 
433             setClipBounds(bounds);
434         }
435         // Instead of clearing clip bounds when moveToTuck is false,
436         // wait until the spring animation finishes.
437     }
438 
onSpringAnimationsEndAction()439     private void onSpringAnimationsEndAction() {
440         if (mShouldShowDockTooltip) {
441             mEduTooltipView.ifPresent(this::removeTooltip);
442             mEduTooltipView = Optional.of(new MenuEduTooltipView(mContext, mMenuViewAppearance));
443             mEduTooltipView.ifPresent(view -> addTooltipView(view,
444                     getContext().getText(R.string.accessibility_floating_button_docking_tooltip),
445                     TooltipType.DOCK));
446 
447             mMenuAnimationController.startTuckedAnimationPreview();
448         }
449 
450         if (!mMenuView.isMoveToTucked()) {
451             setClipBounds(null);
452         }
453         mMenuView.onArrivalAtPosition(false);
454     }
455 
dispatchAccessibilityAction(int id)456     void dispatchAccessibilityAction(int id) {
457         if (id == R.id.action_remove_menu) {
458             if (Flags.floatingMenuDragToHide()) {
459                 hideMenuAndShowNotification();
460             } else {
461                 hideMenuAndShowMessage();
462             }
463             mMenuView.incrementTexMetric(TEX_METRIC_DISMISS);
464         } else if (id == R.id.action_edit
465                 && Flags.floatingMenuDragToEdit()) {
466             gotoEditScreen();
467             mMenuView.incrementTexMetric(TEX_METRIC_EDIT);
468         }
469         mDismissView.hide();
470         mDragToInteractView.hide();
471         mDragToInteractAnimationController.animateInteractMenu(
472                 id, /* scaleUp= */ false);
473     }
474 
gotoEditScreen()475     void gotoEditScreen() {
476         if (!Flags.floatingMenuDragToEdit()) {
477             return;
478         }
479         mMenuAnimationController.flingMenuThenSpringToEdge(
480                 mMenuView.getMenuPosition(), 100f, 0f);
481 
482         Intent intent = getIntentForEditScreen();
483         PackageManager packageManager = getContext().getPackageManager();
484         List<ResolveInfo> activities = packageManager.queryIntentActivities(intent,
485                 PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY));
486         if (!activities.isEmpty()) {
487             mContext.startActivityAsUser(intent, UserHandle.CURRENT);
488             mStatusBarManager.collapsePanels();
489         }
490     }
491 
getIntentForEditScreen()492     Intent getIntentForEditScreen() {
493         List<String> targets = new SettingsStringUtil.ColonDelimitedSet.OfStrings(
494                 mSecureSettings.getStringForUser(
495                         Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
496                         UserHandle.USER_CURRENT)).stream().toList();
497 
498         Intent intent = new Intent(
499                 Settings.ACTION_ACCESSIBILITY_SHORTCUT_SETTINGS);
500         Bundle args = new Bundle();
501         Bundle fragmentArgs = new Bundle();
502         fragmentArgs.putStringArray("targets", targets.toArray(new String[0]));
503         args.putBundle(":settings:show_fragment_args", fragmentArgs);
504         intent.replaceExtras(args);
505         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
506         return intent;
507     }
508 
getMigrationMessage()509     private CharSequence getMigrationMessage() {
510         final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS);
511         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
512         intent.putExtra(Intent.EXTRA_COMPONENT_NAME,
513                 ACCESSIBILITY_BUTTON_COMPONENT_NAME.flattenToShortString());
514 
515         final AnnotationLinkSpan.LinkInfo linkInfo = new AnnotationLinkSpan.LinkInfo(
516                 AnnotationLinkSpan.LinkInfo.DEFAULT_ANNOTATION,
517                 v -> {
518                     getContext().startActivity(intent);
519                     mEduTooltipView.ifPresent(this::removeTooltip);
520                 });
521 
522         final int textResId = R.string.accessibility_floating_button_migration_tooltip;
523 
524         return AnnotationLinkSpan.linkify(getContext().getText(textResId), linkInfo);
525     }
526 
addTooltipView(MenuEduTooltipView tooltipView, CharSequence message, CharSequence tag)527     private void addTooltipView(MenuEduTooltipView tooltipView, CharSequence message,
528             CharSequence tag) {
529         addView(tooltipView, LayerIndex.TOOLTIP_VIEW);
530 
531         tooltipView.show(message);
532         tooltipView.setTag(tag);
533 
534         mMenuListViewTouchHandler.setOnActionDownEndListener(
535                 () -> mEduTooltipView.ifPresent(this::removeTooltip));
536     }
537 
removeTooltip(View tooltipView)538     private void removeTooltip(View tooltipView) {
539         if (tooltipView.getTag().equals(TooltipType.MIGRATION)) {
540             mMenuViewModel.updateMigrationTooltipVisibility(/* visible= */ false);
541             mIsMigrationTooltipShowing = false;
542         }
543 
544         if (tooltipView.getTag().equals(TooltipType.DOCK)) {
545             mMenuViewModel.updateDockTooltipVisibility(/* hasSeen= */ true);
546             mMenuView.clearAnimation();
547             mShouldShowDockTooltip = false;
548         }
549 
550         removeView(tooltipView);
551 
552         mMenuListViewTouchHandler.setOnActionDownEndListener(null);
553         mEduTooltipView = Optional.empty();
554     }
555 
556     @VisibleForTesting
hideMenuAndShowMessage()557     void hideMenuAndShowMessage() {
558         final int delayTime = mAccessibilityManager.getRecommendedTimeoutMillis(
559                 SHOW_MESSAGE_DELAY_MS,
560                 AccessibilityManager.FLAG_CONTENT_TEXT
561                         | AccessibilityManager.FLAG_CONTENT_CONTROLS);
562         mHandler.postDelayed(mDismissMenuAction, delayTime);
563         mMessageView.setVisibility(VISIBLE);
564         mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE));
565     }
566 
567     @VisibleForTesting
hideMenuAndShowNotification()568     void hideMenuAndShowNotification() {
569         mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE));
570         showNotification();
571     }
572 
showNotification()573     private void showNotification() {
574         registerReceiverIfNeeded();
575         if (!mIsNotificationShown) {
576             mNotificationManager.notify(
577                     SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN,
578                     mNotificationFactory.createHiddenNotification());
579             mIsNotificationShown = true;
580         }
581     }
582 
dismissNotification()583     private void dismissNotification() {
584         unregisterReceiverIfNeeded();
585         if (mIsNotificationShown) {
586             mNotificationManager.cancel(
587                     SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN);
588             mIsNotificationShown = false;
589         }
590     }
591 
registerReceiverIfNeeded()592     private void registerReceiverIfNeeded() {
593         if (mNotificationActionReceiver != null) {
594             return;
595         }
596         mNotificationActionReceiver = new MenuNotificationActionReceiver();
597         final IntentFilter intentFilter = new IntentFilter();
598         intentFilter.addAction(ACTION_UNDO);
599         intentFilter.addAction(ACTION_DELETE);
600         getContext().registerReceiver(mNotificationActionReceiver, intentFilter,
601                 Context.RECEIVER_EXPORTED);
602     }
603 
unregisterReceiverIfNeeded()604     private void unregisterReceiverIfNeeded() {
605         if (mNotificationActionReceiver == null) {
606             return;
607         }
608         getContext().unregisterReceiver(mNotificationActionReceiver);
609         mNotificationActionReceiver = null;
610     }
611 
undo()612     private void undo() {
613         mHandler.removeCallbacksAndMessages(/* token= */ null);
614         mMessageView.setVisibility(GONE);
615         mMenuView.onEdgeChanged();
616         mMenuView.onPositionChanged();
617         mMenuView.setVisibility(VISIBLE);
618         mMenuAnimationController.startGrowAnimation();
619     }
620 
621     @VisibleForTesting
getDragToInteractAnimationController()622     DragToInteractAnimationController getDragToInteractAnimationController() {
623         return mDragToInteractAnimationController;
624     }
625 
626     private class MenuNotificationActionReceiver extends BroadcastReceiver {
627         @Override
onReceive(Context context, Intent intent)628         public void onReceive(Context context, Intent intent) {
629             String action = intent.getAction();
630             if (ACTION_UNDO.equals(action)) {
631                 dismissNotification();
632                 undo();
633             } else if (ACTION_DELETE.equals(action)) {
634                 dismissNotification();
635                 mDismissMenuAction.run();
636             }
637         }
638     }
639 }
640