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 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY; 21 22 import static androidx.core.view.WindowInsetsCompat.Type; 23 24 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_BUTTON_COMPONENT_NAME; 25 import static com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType.INVISIBLE_TOGGLE; 26 import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType; 27 import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState; 28 import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.Index; 29 import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat; 30 31 import android.accessibilityservice.AccessibilityServiceInfo; 32 import android.annotation.IntDef; 33 import android.annotation.StringDef; 34 import android.annotation.SuppressLint; 35 import android.content.ComponentCallbacks; 36 import android.content.ComponentName; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.content.res.Configuration; 40 import android.content.res.Resources; 41 import android.graphics.Rect; 42 import android.os.Handler; 43 import android.os.Looper; 44 import android.os.UserHandle; 45 import android.provider.Settings; 46 import android.view.MotionEvent; 47 import android.view.View; 48 import android.view.ViewTreeObserver; 49 import android.view.WindowInsets; 50 import android.view.WindowManager; 51 import android.view.WindowMetrics; 52 import android.view.accessibility.AccessibilityManager; 53 import android.widget.FrameLayout; 54 import android.widget.TextView; 55 56 import androidx.annotation.NonNull; 57 import androidx.lifecycle.Observer; 58 59 import com.android.internal.accessibility.dialog.AccessibilityTarget; 60 import com.android.internal.annotations.VisibleForTesting; 61 import com.android.internal.util.Preconditions; 62 import com.android.systemui.R; 63 import com.android.systemui.util.settings.SecureSettings; 64 import com.android.wm.shell.bubbles.DismissViewUtils; 65 import com.android.wm.shell.common.bubbles.DismissView; 66 import com.android.wm.shell.common.magnetictarget.MagnetizedObject; 67 68 import java.lang.annotation.Retention; 69 import java.lang.annotation.RetentionPolicy; 70 import java.util.List; 71 import java.util.Optional; 72 73 /** 74 * The basic interactions with the child views {@link MenuView}, {@link DismissView}, and 75 * {@link MenuMessageView}. When dragging the menu view, the dismissed view would be shown at the 76 * same time. If the menu view overlaps on the dismissed circle view and drops out, the menu 77 * message view would be shown and allowed users to undo it. 78 */ 79 @SuppressLint("ViewConstructor") 80 class MenuViewLayer extends FrameLayout implements 81 ViewTreeObserver.OnComputeInternalInsetsListener, View.OnClickListener, ComponentCallbacks { 82 private static final int SHOW_MESSAGE_DELAY_MS = 3000; 83 84 private final WindowManager mWindowManager; 85 private final MenuView mMenuView; 86 private final MenuListViewTouchHandler mMenuListViewTouchHandler; 87 private final MenuMessageView mMessageView; 88 private final DismissView mDismissView; 89 private final MenuViewAppearance mMenuViewAppearance; 90 private final MenuAnimationController mMenuAnimationController; 91 private final AccessibilityManager mAccessibilityManager; 92 private final Handler mHandler = new Handler(Looper.getMainLooper()); 93 private final IAccessibilityFloatingMenu mFloatingMenu; 94 private final SecureSettings mSecureSettings; 95 private final DismissAnimationController mDismissAnimationController; 96 private final MenuViewModel mMenuViewModel; 97 private final Observer<Boolean> mDockTooltipObserver = 98 this::onDockTooltipVisibilityChanged; 99 private final Observer<Boolean> mMigrationTooltipObserver = 100 this::onMigrationTooltipVisibilityChanged; 101 private final Rect mImeInsetsRect = new Rect(); 102 private boolean mIsMigrationTooltipShowing; 103 private boolean mShouldShowDockTooltip; 104 private Optional<MenuEduTooltipView> mEduTooltipView = Optional.empty(); 105 106 @IntDef({ 107 LayerIndex.MENU_VIEW, 108 LayerIndex.DISMISS_VIEW, 109 LayerIndex.MESSAGE_VIEW, 110 LayerIndex.TOOLTIP_VIEW, 111 }) 112 @Retention(RetentionPolicy.SOURCE) 113 @interface LayerIndex { 114 int MENU_VIEW = 0; 115 int DISMISS_VIEW = 1; 116 int MESSAGE_VIEW = 2; 117 int TOOLTIP_VIEW = 3; 118 } 119 120 @StringDef({ 121 TooltipType.MIGRATION, 122 TooltipType.DOCK, 123 }) 124 @Retention(RetentionPolicy.SOURCE) 125 @interface TooltipType { 126 String MIGRATION = "migration"; 127 String DOCK = "dock"; 128 } 129 130 @VisibleForTesting 131 final Runnable mDismissMenuAction = new Runnable() { 132 @Override 133 public void run() { 134 mSecureSettings.putStringForUser( 135 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, /* value= */ "", 136 UserHandle.USER_CURRENT); 137 138 final List<ComponentName> hardwareKeyShortcutComponents = 139 mAccessibilityManager.getAccessibilityShortcutTargets( 140 ACCESSIBILITY_SHORTCUT_KEY) 141 .stream() 142 .map(ComponentName::unflattenFromString) 143 .toList(); 144 145 // Should disable the corresponding service when the fragment type is 146 // INVISIBLE_TOGGLE, which will enable service when the shortcut is on. 147 final List<AccessibilityServiceInfo> serviceInfoList = 148 mAccessibilityManager.getEnabledAccessibilityServiceList( 149 AccessibilityServiceInfo.FEEDBACK_ALL_MASK); 150 serviceInfoList.forEach(info -> { 151 if (getAccessibilityServiceFragmentType(info) != INVISIBLE_TOGGLE) { 152 return; 153 } 154 155 final ComponentName serviceComponentName = info.getComponentName(); 156 if (hardwareKeyShortcutComponents.contains(serviceComponentName)) { 157 return; 158 } 159 160 setAccessibilityServiceState(getContext(), serviceComponentName, /* enabled= */ 161 false); 162 }); 163 164 mFloatingMenu.hide(); 165 } 166 }; 167 MenuViewLayer(@onNull Context context, WindowManager windowManager, AccessibilityManager accessibilityManager, IAccessibilityFloatingMenu floatingMenu, SecureSettings secureSettings)168 MenuViewLayer(@NonNull Context context, WindowManager windowManager, 169 AccessibilityManager accessibilityManager, IAccessibilityFloatingMenu floatingMenu, 170 SecureSettings secureSettings) { 171 super(context); 172 173 // Simplifies the translation positioning and animations 174 setLayoutDirection(LAYOUT_DIRECTION_LTR); 175 176 mWindowManager = windowManager; 177 mAccessibilityManager = accessibilityManager; 178 mFloatingMenu = floatingMenu; 179 mSecureSettings = secureSettings; 180 181 mMenuViewModel = new MenuViewModel(context, accessibilityManager, secureSettings); 182 mMenuViewAppearance = new MenuViewAppearance(context, windowManager); 183 mMenuView = new MenuView(context, mMenuViewModel, mMenuViewAppearance); 184 mMenuAnimationController = mMenuView.getMenuAnimationController(); 185 mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage); 186 mMenuAnimationController.setSpringAnimationsEndAction(this::onSpringAnimationsEndAction); 187 mDismissView = new DismissView(context); 188 DismissViewUtils.setup(mDismissView); 189 mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView); 190 mDismissAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() { 191 @Override 192 public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { 193 mDismissAnimationController.animateDismissMenu(/* scaleUp= */ true); 194 } 195 196 @Override 197 public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, 198 float velocityX, float velocityY, boolean wasFlungOut) { 199 mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false); 200 } 201 202 @Override 203 public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { 204 hideMenuAndShowMessage(); 205 mDismissView.hide(); 206 mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false); 207 } 208 }); 209 210 mMenuListViewTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController, 211 mDismissAnimationController); 212 mMenuView.addOnItemTouchListenerToList(mMenuListViewTouchHandler); 213 214 mMessageView = new MenuMessageView(context); 215 216 mMenuView.setOnTargetFeaturesChangeListener(newTargetFeatures -> { 217 if (newTargetFeatures.size() < 1) { 218 return; 219 } 220 221 // During the undo action period, the pending action will be canceled and undo back 222 // to the previous state if users did any action related to the accessibility features. 223 if (mMessageView.getVisibility() == VISIBLE) { 224 undo(); 225 } 226 227 final TextView messageText = (TextView) mMessageView.getChildAt(Index.TEXT_VIEW); 228 messageText.setText(getMessageText(newTargetFeatures)); 229 }); 230 231 addView(mMenuView, LayerIndex.MENU_VIEW); 232 addView(mDismissView, LayerIndex.DISMISS_VIEW); 233 addView(mMessageView, LayerIndex.MESSAGE_VIEW); 234 } 235 236 @Override onConfigurationChanged(@onNull Configuration newConfig)237 public void onConfigurationChanged(@NonNull Configuration newConfig) { 238 mDismissView.updateResources(); 239 mDismissAnimationController.updateResources(); 240 } 241 242 @Override onLowMemory()243 public void onLowMemory() { 244 // Do nothing. 245 } 246 getMessageText(List<AccessibilityTarget> newTargetFeatures)247 private String getMessageText(List<AccessibilityTarget> newTargetFeatures) { 248 Preconditions.checkArgument(newTargetFeatures.size() > 0, 249 "The list should at least have one feature."); 250 251 final int featuresSize = newTargetFeatures.size(); 252 final Resources resources = getResources(); 253 if (featuresSize == 1) { 254 return resources.getString( 255 R.string.accessibility_floating_button_undo_message_label_text, 256 newTargetFeatures.get(0).getLabel()); 257 } 258 259 return icuMessageFormat(resources, 260 R.string.accessibility_floating_button_undo_message_number_text, featuresSize); 261 } 262 263 @Override onInterceptTouchEvent(MotionEvent event)264 public boolean onInterceptTouchEvent(MotionEvent event) { 265 if (mMenuView.maybeMoveOutEdgeAndShow((int) event.getX(), (int) event.getY())) { 266 return true; 267 } 268 269 return super.onInterceptTouchEvent(event); 270 } 271 272 @Override onAttachedToWindow()273 protected void onAttachedToWindow() { 274 super.onAttachedToWindow(); 275 276 mMenuView.show(); 277 setOnClickListener(this); 278 setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets)); 279 getViewTreeObserver().addOnComputeInternalInsetsListener(this); 280 mMenuViewModel.getDockTooltipVisibilityData().observeForever(mDockTooltipObserver); 281 mMenuViewModel.getMigrationTooltipVisibilityData().observeForever( 282 mMigrationTooltipObserver); 283 mMessageView.setUndoListener(view -> undo()); 284 getContext().registerComponentCallbacks(this); 285 } 286 287 @Override onDetachedFromWindow()288 protected void onDetachedFromWindow() { 289 super.onDetachedFromWindow(); 290 291 mMenuView.hide(); 292 setOnClickListener(null); 293 setOnApplyWindowInsetsListener(null); 294 getViewTreeObserver().removeOnComputeInternalInsetsListener(this); 295 mMenuViewModel.getDockTooltipVisibilityData().removeObserver(mDockTooltipObserver); 296 mMenuViewModel.getMigrationTooltipVisibilityData().removeObserver( 297 mMigrationTooltipObserver); 298 mHandler.removeCallbacksAndMessages(/* token= */ null); 299 getContext().unregisterComponentCallbacks(this); 300 } 301 302 @Override onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)303 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { 304 inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 305 306 if (mEduTooltipView.isPresent()) { 307 final int x = (int) getX(); 308 final int y = (int) getY(); 309 inoutInfo.touchableRegion.union(new Rect(x, y, x + getWidth(), y + getHeight())); 310 } 311 } 312 313 @Override onClick(View v)314 public void onClick(View v) { 315 mEduTooltipView.ifPresent(this::removeTooltip); 316 } 317 onWindowInsetsApplied(WindowInsets insets)318 private WindowInsets onWindowInsetsApplied(WindowInsets insets) { 319 final WindowMetrics windowMetrics = mWindowManager.getCurrentWindowMetrics(); 320 final WindowInsets windowInsets = windowMetrics.getWindowInsets(); 321 final Rect imeInsetsRect = windowInsets.getInsets(ime()).toRect(); 322 if (!imeInsetsRect.equals(mImeInsetsRect)) { 323 final Rect windowBounds = new Rect(windowMetrics.getBounds()); 324 final Rect systemBarsAndDisplayCutoutInsetsRect = 325 windowInsets.getInsetsIgnoringVisibility( 326 Type.systemBars() | Type.displayCutout()).toRect(); 327 final float imeTop = 328 windowBounds.height() - systemBarsAndDisplayCutoutInsetsRect.top 329 - imeInsetsRect.bottom; 330 331 mMenuViewAppearance.onImeVisibilityChanged(windowInsets.isVisible(ime()), imeTop); 332 333 mMenuView.onEdgeChanged(); 334 mMenuView.onPositionChanged(); 335 336 mImeInsetsRect.set(imeInsetsRect); 337 } 338 339 return insets; 340 } 341 onMigrationTooltipVisibilityChanged(boolean visible)342 private void onMigrationTooltipVisibilityChanged(boolean visible) { 343 mIsMigrationTooltipShowing = visible; 344 345 if (mIsMigrationTooltipShowing) { 346 mEduTooltipView = Optional.of(new MenuEduTooltipView(mContext, mMenuViewAppearance)); 347 mEduTooltipView.ifPresent( 348 view -> addTooltipView(view, getMigrationMessage(), TooltipType.MIGRATION)); 349 } 350 } 351 onDockTooltipVisibilityChanged(boolean hasSeenTooltip)352 private void onDockTooltipVisibilityChanged(boolean hasSeenTooltip) { 353 mShouldShowDockTooltip = !hasSeenTooltip; 354 } 355 onSpringAnimationsEndAction()356 private void onSpringAnimationsEndAction() { 357 if (mShouldShowDockTooltip) { 358 mEduTooltipView = Optional.of(new MenuEduTooltipView(mContext, mMenuViewAppearance)); 359 mEduTooltipView.ifPresent(view -> addTooltipView(view, 360 getContext().getText(R.string.accessibility_floating_button_docking_tooltip), 361 TooltipType.DOCK)); 362 363 mMenuAnimationController.startTuckedAnimationPreview(); 364 } 365 } 366 getMigrationMessage()367 private CharSequence getMigrationMessage() { 368 final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS); 369 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 370 intent.putExtra(Intent.EXTRA_COMPONENT_NAME, 371 ACCESSIBILITY_BUTTON_COMPONENT_NAME.flattenToShortString()); 372 373 final AnnotationLinkSpan.LinkInfo linkInfo = new AnnotationLinkSpan.LinkInfo( 374 AnnotationLinkSpan.LinkInfo.DEFAULT_ANNOTATION, 375 v -> { 376 getContext().startActivity(intent); 377 mEduTooltipView.ifPresent(this::removeTooltip); 378 }); 379 380 final int textResId = R.string.accessibility_floating_button_migration_tooltip; 381 382 return AnnotationLinkSpan.linkify(getContext().getText(textResId), linkInfo); 383 } 384 addTooltipView(MenuEduTooltipView tooltipView, CharSequence message, CharSequence tag)385 private void addTooltipView(MenuEduTooltipView tooltipView, CharSequence message, 386 CharSequence tag) { 387 addView(tooltipView, LayerIndex.TOOLTIP_VIEW); 388 389 tooltipView.show(message); 390 tooltipView.setTag(tag); 391 392 mMenuListViewTouchHandler.setOnActionDownEndListener( 393 () -> mEduTooltipView.ifPresent(this::removeTooltip)); 394 } 395 removeTooltip(View tooltipView)396 private void removeTooltip(View tooltipView) { 397 if (tooltipView.getTag().equals(TooltipType.MIGRATION)) { 398 mMenuViewModel.updateMigrationTooltipVisibility(/* visible= */ false); 399 mIsMigrationTooltipShowing = false; 400 } 401 402 if (tooltipView.getTag().equals(TooltipType.DOCK)) { 403 mMenuViewModel.updateDockTooltipVisibility(/* hasSeen= */ true); 404 mMenuView.clearAnimation(); 405 mShouldShowDockTooltip = false; 406 } 407 408 removeView(tooltipView); 409 410 mMenuListViewTouchHandler.setOnActionDownEndListener(null); 411 mEduTooltipView = Optional.empty(); 412 } 413 hideMenuAndShowMessage()414 private void hideMenuAndShowMessage() { 415 final int delayTime = mAccessibilityManager.getRecommendedTimeoutMillis( 416 SHOW_MESSAGE_DELAY_MS, 417 AccessibilityManager.FLAG_CONTENT_TEXT 418 | AccessibilityManager.FLAG_CONTENT_CONTROLS); 419 mHandler.postDelayed(mDismissMenuAction, delayTime); 420 mMessageView.setVisibility(VISIBLE); 421 mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE)); 422 } 423 undo()424 private void undo() { 425 mHandler.removeCallbacksAndMessages(/* token= */ null); 426 mMessageView.setVisibility(GONE); 427 mMenuView.onEdgeChanged(); 428 mMenuView.onPositionChanged(); 429 mMenuView.setVisibility(VISIBLE); 430 mMenuAnimationController.startGrowAnimation(); 431 } 432 } 433