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