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