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