1 // Copyright 2013 The Flutter Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package io.flutter.view; 6 7 import android.annotation.TargetApi; 8 import android.content.ContentResolver; 9 import android.database.ContentObserver; 10 import android.graphics.Rect; 11 import android.net.Uri; 12 import android.opengl.Matrix; 13 import android.os.Build; 14 import android.os.Bundle; 15 import android.os.Handler; 16 import android.provider.Settings; 17 import android.support.annotation.Nullable; 18 import android.support.annotation.NonNull; 19 import android.support.annotation.RequiresApi; 20 import android.util.Log; 21 import android.view.MotionEvent; 22 import android.view.View; 23 import android.view.WindowInsets; 24 import android.view.accessibility.AccessibilityEvent; 25 import android.view.accessibility.AccessibilityManager; 26 import android.view.accessibility.AccessibilityNodeInfo; 27 import android.view.accessibility.AccessibilityNodeProvider; 28 29 import io.flutter.BuildConfig; 30 import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; 31 import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate; 32 import io.flutter.util.Predicate; 33 34 import java.nio.ByteBuffer; 35 import java.nio.ByteOrder; 36 import java.util.*; 37 38 /** 39 * Bridge between Android's OS accessibility system and Flutter's accessibility system. 40 * 41 * An {@code AccessibilityBridge} requires: 42 * <ul> 43 * <li>A real Android {@link View}, called the {@link #rootAccessibilityView}, which contains a 44 * Flutter UI. The {@link #rootAccessibilityView} is required at the time of 45 * {@code AccessibilityBridge}'s instantiation and is held for the duration of 46 * {@code AccessibilityBridge}'s lifespan. {@code AccessibilityBridge} invokes various 47 * accessibility methods on the {@link #rootAccessibilityView}, e.g., 48 * {@link View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)}. The 49 * {@link #rootAccessibilityView} is expected to notify the {@code AccessibilityBridge} of 50 * relevant interactions: {@link #onAccessibilityHoverEvent(MotionEvent)}, {@link #reset()}, 51 * {@link #updateSemantics(ByteBuffer, String[])}, and {@link #updateCustomAccessibilityActions(ByteBuffer, String[])}</li> 52 * <li>An {@link AccessibilityChannel} that is connected to the running Flutter app.</li> 53 * <li>Android's {@link AccessibilityManager} to query and listen for accessibility settings.</li> 54 * <li>Android's {@link ContentResolver} to listen for changes to system animation settings.</li> 55 * </ul> 56 * 57 * The {@code AccessibilityBridge} causes Android to treat Flutter {@code SemanticsNode}s as if 58 * they were accessible Android {@link View}s. Accessibility requests may be sent from 59 * a Flutter widget to the Android OS, as if it were an Android {@link View}, and 60 * accessibility events may be consumed by a Flutter widget, as if it were an Android 61 * {@link View}. {@code AccessibilityBridge} refers to Flutter's accessible widgets as 62 * "virtual views" and identifies them with "virtual view IDs". 63 */ 64 public class AccessibilityBridge extends AccessibilityNodeProvider { 65 private static final String TAG = "AccessibilityBridge"; 66 67 // Constants from higher API levels. 68 // TODO(goderbauer): Get these from Android Support Library when 69 // https://github.com/flutter/flutter/issues/11099 is resolved. 70 private static final int ACTION_SHOW_ON_SCREEN = 16908342; // API level 23 71 72 private static final float SCROLL_EXTENT_FOR_INFINITY = 100000.0f; 73 private static final float SCROLL_POSITION_CAP_FOR_INFINITY = 70000.0f; 74 private static final int ROOT_NODE_ID = 0; 75 76 // The minimal ID for an engine generated AccessibilityNodeInfo. 77 // 78 // The AccessibilityNodeInfo node IDs are generated by the framework for most Flutter semantic nodes. 79 // When embedding platform views, the framework does not have the accessibility information for the embedded view; 80 // in this case the engine generates AccessibilityNodeInfo that mirrors the a11y information exposed by the platform 81 // view. To avoid the need of synchronizing the framework and engine mechanisms for generating the next ID, we split 82 // the 32bit range of virtual node IDs into 2. The least significant 16 bits are used for framework generated IDs 83 // and the most significant 16 bits are used for engine generated IDs. 84 private static final int MIN_ENGINE_GENERATED_NODE_ID = 1<<16; 85 86 /// Value is derived from ACTION_TYPE_MASK in AccessibilityNodeInfo.java 87 private static int FIRST_RESOURCE_ID = 267386881; 88 89 // Real Android View, which internally holds a Flutter UI. 90 @NonNull 91 private final View rootAccessibilityView; 92 93 // The accessibility communication API between Flutter's Android embedding and 94 // the Flutter framework. 95 @NonNull 96 private final AccessibilityChannel accessibilityChannel; 97 98 // Android's {@link AccessibilityManager}, which we can query to see if accessibility is 99 // turned on, as well as listen for changes to accessibility's activation. 100 @NonNull 101 private final AccessibilityManager accessibilityManager; 102 103 @NonNull 104 private final AccessibilityViewEmbedder accessibilityViewEmbedder; 105 106 // The delegate for interacting with embedded platform views. Used to embed accessibility data for an embedded 107 // view in the accessibility tree. 108 @NonNull 109 private final PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate; 110 111 // Android's {@link ContentResolver}, which is used to observe the global TRANSITION_ANIMATION_SCALE, 112 // which determines whether Flutter's animations should be enabled or disabled for accessibility 113 // purposes. 114 @NonNull 115 private final ContentResolver contentResolver; 116 117 // The entire Flutter semantics tree of the running Flutter app, stored as a Map 118 // from each SemanticsNode's ID to a Java representation of a Flutter SemanticsNode. 119 // 120 // Flutter's semantics tree is cached here because Android might ask for information about 121 // a given SemanticsNode at any moment in time. Caching the tree allows for immediate 122 // response to Android's request. 123 // 124 // The structure of flutterSemanticsTree may be 1 or 2 frames behind the Flutter app 125 // due to the time required to communicate tree changes from Flutter to Android. 126 // 127 // See the Flutter docs on SemanticsNode: 128 // https://docs.flutter.io/flutter/semantics/SemanticsNode-class.html 129 @NonNull 130 private final Map<Integer, SemanticsNode> flutterSemanticsTree = new HashMap<>(); 131 132 // The set of all custom Flutter accessibility actions that are present in the running 133 // Flutter app, stored as a Map from each action's ID to the definition of the custom accessibility 134 // action. 135 // 136 // Flutter and Android support a number of built-in accessibility actions. However, these 137 // predefined actions are not always sufficient for a desired interaction. Android facilitates 138 // custom accessibility actions, https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction. 139 // Flutter supports custom accessibility actions via {@code customSemanticsActions} within 140 // a {@code Semantics} widget, https://docs.flutter.io/flutter/widgets/Semantics-class.html. 141 // {@code customAccessibilityActions} are an Android-side cache of all custom accessibility 142 // types declared within the running Flutter app. 143 // 144 // Custom accessibility actions are comprised of only a few fields, and therefore it is likely 145 // that a given app may define the same custom accessibility action many times. Identical 146 // custom accessibility actions are de-duped such that {@code customAccessibilityActions} only 147 // caches unique custom accessibility actions. 148 // 149 // See the Android documentation for custom accessibility actions: 150 // https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction 151 // 152 // See the Flutter documentation for the Semantics widget: 153 // https://docs.flutter.io/flutter/widgets/Semantics-class.html 154 @NonNull 155 private final Map<Integer, CustomAccessibilityAction> customAccessibilityActions = new HashMap<>(); 156 157 // The {@code SemanticsNode} within Flutter that currently has the focus of Android's 158 // accessibility system. 159 // 160 // This is null when a node embedded by the AccessibilityViewEmbedder has the focus. 161 @Nullable 162 private SemanticsNode accessibilityFocusedSemanticsNode; 163 164 // The virtual ID of the currently embedded node with accessibility focus. 165 // 166 // This is the ID of a node generated by the AccessibilityViewEmbedder if an embedded node is focused, 167 // null otherwise. 168 private Integer embeddedAccessibilityFocusedNodeId; 169 170 // The virtual ID of the currently embedded node with input focus. 171 // 172 // This is the ID of a node generated by the AccessibilityViewEmbedder if an embedded node is focused, 173 // null otherwise. 174 private Integer embeddedInputFocusedNodeId; 175 176 // The accessibility features that should currently be active within Flutter, represented as 177 // a bitmask whose values comes from {@link AccessibilityFeature}. 178 private int accessibilityFeatureFlags = 0; 179 180 // The {@code SemanticsNode} within Flutter that currently has the focus of Android's input 181 // system. 182 // 183 // Input focus is independent of accessibility focus. It is possible that accessibility focus 184 // and input focus target the same {@code SemanticsNode}, but it is also possible that one 185 // {@code SemanticsNode} has input focus while a different {@code SemanticsNode} has 186 // accessibility focus. For example, a user may use a D-Pad to navigate to a text field, giving 187 // it accessibility focus, and then enable input on that text field, giving it input focus. Then 188 // the user moves the accessibility focus to a nearby label to get info about the label, while 189 // maintaining input focus on the original text field. 190 @Nullable 191 private SemanticsNode inputFocusedSemanticsNode; 192 193 // The widget within Flutter that currently sits beneath a cursor, e.g, 194 // beneath a stylus or mouse cursor. 195 @Nullable 196 private SemanticsNode hoveredObject; 197 198 // A Java/Android cached representation of the Flutter app's navigation stack. The Flutter 199 // navigation stack is tracked so that accessibility announcements can be made during Flutter's 200 // navigation changes. 201 // TODO(mattcarroll): take this cache into account for new routing solution so accessibility does 202 // not get left behind. 203 @NonNull 204 private final List<Integer> flutterNavigationStack = new ArrayList<>(); 205 206 // TODO(mattcarroll): why do we need previouseRouteId if we have flutterNavigationStack 207 private int previousRouteId = ROOT_NODE_ID; 208 209 // Tracks the left system inset of the screen because Flutter needs to manually adjust 210 // accessibility positioning when in reverse-landscape. This is an Android bug that Flutter 211 // is solving for itself. 212 @NonNull 213 private Integer lastLeftFrameInset = 0; 214 215 @Nullable 216 private OnAccessibilityChangeListener onAccessibilityChangeListener; 217 218 // Handler for all messages received from Flutter via the {@code accessibilityChannel} 219 private final AccessibilityChannel.AccessibilityMessageHandler accessibilityMessageHandler = new AccessibilityChannel.AccessibilityMessageHandler() { 220 /** 221 * The Dart application would like the given {@code message} to be announced. 222 */ 223 @Override 224 public void announce(@NonNull String message) { 225 rootAccessibilityView.announceForAccessibility(message); 226 } 227 228 /** 229 * The user has tapped on the widget with the given {@code nodeId}. 230 */ 231 @Override 232 public void onTap(int nodeId) { 233 sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_CLICKED); 234 } 235 236 /** 237 * The user has long pressed on the widget with the given {@code nodeId}. 238 */ 239 @Override 240 public void onLongPress(int nodeId) { 241 sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); 242 } 243 244 /** 245 * The user has opened a tooltip. 246 */ 247 @Override 248 public void onTooltip(@NonNull String message) { 249 AccessibilityEvent e = obtainAccessibilityEvent(ROOT_NODE_ID, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 250 e.getText().add(message); 251 sendAccessibilityEvent(e); 252 } 253 254 /** 255 * New custom accessibility actions exist in Flutter. Update our Android-side cache. 256 */ 257 @Override 258 public void updateCustomAccessibilityActions(ByteBuffer buffer, String[] strings) { 259 buffer.order(ByteOrder.LITTLE_ENDIAN); 260 AccessibilityBridge.this.updateCustomAccessibilityActions(buffer, strings); 261 } 262 263 /** 264 * Flutter's semantics tree has changed. Update our Android-side cache. 265 */ 266 @Override 267 public void updateSemantics(ByteBuffer buffer, String[] strings) { 268 buffer.order(ByteOrder.LITTLE_ENDIAN); 269 AccessibilityBridge.this.updateSemantics(buffer, strings); 270 } 271 }; 272 273 // Listener that is notified when accessibility is turned on/off. 274 private final AccessibilityManager.AccessibilityStateChangeListener accessibilityStateChangeListener = new AccessibilityManager.AccessibilityStateChangeListener() { 275 @Override 276 public void onAccessibilityStateChanged(boolean accessibilityEnabled) { 277 if (accessibilityEnabled) { 278 accessibilityChannel.setAccessibilityMessageHandler(accessibilityMessageHandler); 279 accessibilityChannel.onAndroidAccessibilityEnabled(); 280 } else { 281 accessibilityChannel.setAccessibilityMessageHandler(null); 282 accessibilityChannel.onAndroidAccessibilityDisabled(); 283 } 284 285 if (onAccessibilityChangeListener != null) { 286 onAccessibilityChangeListener.onAccessibilityChanged( 287 accessibilityEnabled, 288 accessibilityManager.isTouchExplorationEnabled() 289 ); 290 } 291 } 292 }; 293 294 // Listener that is notified when accessibility touch exploration is turned on/off. 295 // This is guarded at instantiation time. 296 @TargetApi(19) 297 @RequiresApi(19) 298 private final AccessibilityManager.TouchExplorationStateChangeListener touchExplorationStateChangeListener; 299 300 // Listener that is notified when the global TRANSITION_ANIMATION_SCALE. When this scale goes 301 // to zero, we instruct Flutter to disable animations. 302 private final ContentObserver animationScaleObserver = new ContentObserver(new Handler()) { 303 @Override 304 public void onChange(boolean selfChange) { 305 this.onChange(selfChange, null); 306 } 307 308 @Override 309 public void onChange(boolean selfChange, Uri uri) { 310 // Retrieve the current value of TRANSITION_ANIMATION_SCALE from the OS. 311 String value = Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1 ? null 312 : Settings.Global.getString( 313 contentResolver, 314 Settings.Global.TRANSITION_ANIMATION_SCALE 315 ); 316 317 boolean shouldAnimationsBeDisabled = value != null && value.equals("0"); 318 if (shouldAnimationsBeDisabled) { 319 accessibilityFeatureFlags |= AccessibilityFeature.DISABLE_ANIMATIONS.value; 320 } else { 321 accessibilityFeatureFlags &= ~AccessibilityFeature.DISABLE_ANIMATIONS.value; 322 } 323 sendLatestAccessibilityFlagsToFlutter(); 324 } 325 }; 326 AccessibilityBridge( @onNull View rootAccessibilityView, @NonNull AccessibilityChannel accessibilityChannel, @NonNull AccessibilityManager accessibilityManager, @NonNull ContentResolver contentResolver, PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate )327 public AccessibilityBridge( 328 @NonNull View rootAccessibilityView, 329 @NonNull AccessibilityChannel accessibilityChannel, 330 @NonNull AccessibilityManager accessibilityManager, 331 @NonNull ContentResolver contentResolver, 332 // This should be @NonNull once the plumbing for io.flutter.embedding.engine.android.FlutterView is done. 333 // TODO(mattcarrol): Add the annotation once the plumbing is done. 334 // https://github.com/flutter/flutter/issues/29618 335 PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate 336 ) { 337 this.rootAccessibilityView = rootAccessibilityView; 338 this.accessibilityChannel = accessibilityChannel; 339 this.accessibilityManager = accessibilityManager; 340 this.contentResolver = contentResolver; 341 this.platformViewsAccessibilityDelegate = platformViewsAccessibilityDelegate; 342 343 // Tell Flutter whether accessibility is initially active or not. Then register a listener 344 // to be notified of changes in the future. 345 accessibilityStateChangeListener.onAccessibilityStateChanged(accessibilityManager.isEnabled()); 346 this.accessibilityManager.addAccessibilityStateChangeListener(accessibilityStateChangeListener); 347 348 // Tell Flutter whether touch exploration is initially active or not. Then register a listener 349 // to be notified of changes in the future. 350 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 351 touchExplorationStateChangeListener = new AccessibilityManager.TouchExplorationStateChangeListener() { 352 @Override 353 public void onTouchExplorationStateChanged(boolean isTouchExplorationEnabled) { 354 if (isTouchExplorationEnabled) { 355 accessibilityFeatureFlags |= AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; 356 } else { 357 onTouchExplorationExit(); 358 accessibilityFeatureFlags &= ~AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; 359 } 360 sendLatestAccessibilityFlagsToFlutter(); 361 362 if (onAccessibilityChangeListener != null) { 363 onAccessibilityChangeListener.onAccessibilityChanged( 364 accessibilityManager.isEnabled(), 365 isTouchExplorationEnabled 366 ); 367 } 368 } 369 }; 370 touchExplorationStateChangeListener.onTouchExplorationStateChanged(accessibilityManager.isTouchExplorationEnabled()); 371 this.accessibilityManager.addTouchExplorationStateChangeListener(touchExplorationStateChangeListener); 372 } else { 373 touchExplorationStateChangeListener = null; 374 } 375 376 // Tell Flutter whether animations should initially be enabled or disabled. Then register a 377 // listener to be notified of changes in the future. 378 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 379 animationScaleObserver.onChange(false); 380 Uri transitionUri = Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE); 381 this.contentResolver.registerContentObserver(transitionUri, false, animationScaleObserver); 382 } 383 384 // platformViewsAccessibilityDelegate should be @NonNull once the plumbing 385 // for io.flutter.embedding.engine.android.FlutterView is done. 386 // TODO(mattcarrol): Remove the null check once the plumbing is done. 387 // https://github.com/flutter/flutter/issues/29618 388 if (platformViewsAccessibilityDelegate != null) { 389 platformViewsAccessibilityDelegate.attachAccessibilityBridge(this); 390 } 391 accessibilityViewEmbedder = new AccessibilityViewEmbedder(rootAccessibilityView, MIN_ENGINE_GENERATED_NODE_ID); 392 } 393 394 /** 395 * Disconnects any listeners and/or delegates that were initialized in {@code AccessibilityBridge}'s 396 * constructor, or added after. 397 * 398 * Do not use this instance after invoking {@code release}. The behavior of any method invoked 399 * on this {@code AccessibilityBridge} after invoking {@code release()} is undefined. 400 */ release()401 public void release() { 402 // platformViewsAccessibilityDelegate should be @NonNull once the plumbing 403 // for io.flutter.embedding.engine.android.FlutterView is done. 404 // TODO(mattcarrol): Remove the null check once the plumbing is done. 405 // https://github.com/flutter/flutter/issues/29618 406 if (platformViewsAccessibilityDelegate != null) { 407 platformViewsAccessibilityDelegate.detachAccessibiltyBridge(); 408 } 409 setOnAccessibilityChangeListener(null); 410 accessibilityManager.removeAccessibilityStateChangeListener(accessibilityStateChangeListener); 411 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 412 accessibilityManager.removeTouchExplorationStateChangeListener(touchExplorationStateChangeListener); 413 } 414 contentResolver.unregisterContentObserver(animationScaleObserver); 415 } 416 417 /** 418 * Returns true if the Android OS currently has accessibility enabled, false otherwise. 419 */ isAccessibilityEnabled()420 public boolean isAccessibilityEnabled() { 421 return accessibilityManager.isEnabled(); 422 } 423 424 /** 425 * Returns true if the Android OS currently has touch exploration enabled, false otherwise. 426 */ isTouchExplorationEnabled()427 public boolean isTouchExplorationEnabled() { 428 return accessibilityManager.isTouchExplorationEnabled(); 429 } 430 431 /** 432 * Sets a listener on this {@code AccessibilityBridge}, which is notified whenever accessibility 433 * activation, or touch exploration activation changes. 434 */ setOnAccessibilityChangeListener(@ullable OnAccessibilityChangeListener listener)435 public void setOnAccessibilityChangeListener(@Nullable OnAccessibilityChangeListener listener) { 436 this.onAccessibilityChangeListener = listener; 437 } 438 439 /** 440 * Sends the current value of {@link #accessibilityFeatureFlags} to Flutter. 441 */ sendLatestAccessibilityFlagsToFlutter()442 private void sendLatestAccessibilityFlagsToFlutter() { 443 accessibilityChannel.setAccessibilityFeatures(accessibilityFeatureFlags); 444 } 445 shouldSetCollectionInfo(final SemanticsNode semanticsNode)446 private boolean shouldSetCollectionInfo(final SemanticsNode semanticsNode) { 447 // TalkBack expects a number of rows and/or columns greater than 0 to announce 448 // in list and out of list. For an infinite or growing list, you have to 449 // specify something > 0 to get "in list" announcements. 450 // TalkBack will also only track one list at a time, so we only want to set this 451 // for a list that contains the current a11y focused semanticsNode - otherwise, if there 452 // are two lists or nested lists, we may end up with announcements for only the last 453 // one that is currently available in the semantics tree. However, we also want 454 // to set it if we're exiting a list to a non-list, so that we can get the "out of list" 455 // announcement when A11y focus moves out of a list and not into another list. 456 return semanticsNode.scrollChildren > 0 457 && (SemanticsNode.nullableHasAncestor(accessibilityFocusedSemanticsNode, o -> o == semanticsNode) 458 || !SemanticsNode.nullableHasAncestor(accessibilityFocusedSemanticsNode, o -> o.hasFlag(Flag.HAS_IMPLICIT_SCROLLING))); 459 } 460 461 /** 462 * Returns {@link AccessibilityNodeInfo} for the view corresponding to the given {@code virtualViewId}. 463 * 464 * This method is invoked by Android's accessibility system when Android needs accessibility info 465 * for a given view. 466 * 467 * When a {@code virtualViewId} of {@link View#NO_ID} is requested, accessibility node info is 468 * returned for our {@link #rootAccessibilityView}. Otherwise, Flutter's semantics tree, 469 * represented by {@link #flutterSemanticsTree}, is searched for a {@link SemanticsNode} with 470 * the given {@code virtualViewId}. If no such {@link SemanticsNode} is found, then this method 471 * returns null. If the desired {@link SemanticsNode} is found, then an {@link AccessibilityNodeInfo} 472 * is obtained from the {@link #rootAccessibilityView}, filled with appropriate info, and then 473 * returned. 474 * 475 * Depending on the type of Flutter {@code SemanticsNode} that is requested, the returned 476 * {@link AccessibilityNodeInfo} pretends that the {@code SemanticsNode} in question comes from 477 * a specialize Android view, e.g., {@link Flag#IS_TEXT_FIELD} maps to {@code android.widget.EditText}, 478 * {@link Flag#IS_BUTTON} maps to {@code android.widget.Button}, and {@link Flag#IS_IMAGE} maps 479 * to {@code android.widget.ImageView}. In the case that no specialized view applies, the 480 * returned {@link AccessibilityNodeInfo} pretends that it represents a {@code android.view.View}. 481 */ 482 @Override 483 @SuppressWarnings("deprecation") createAccessibilityNodeInfo(int virtualViewId)484 public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { 485 if (virtualViewId >= MIN_ENGINE_GENERATED_NODE_ID) { 486 // The node is in the engine generated range, and is provided by the accessibility view embedder. 487 return accessibilityViewEmbedder.createAccessibilityNodeInfo(virtualViewId); 488 } 489 490 if (virtualViewId == View.NO_ID) { 491 AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(rootAccessibilityView); 492 rootAccessibilityView.onInitializeAccessibilityNodeInfo(result); 493 // TODO(mattcarroll): what does it mean for the semantics tree to contain or not contain 494 // the root node ID? 495 if (flutterSemanticsTree.containsKey(ROOT_NODE_ID)) { 496 result.addChild(rootAccessibilityView, ROOT_NODE_ID); 497 } 498 return result; 499 } 500 501 SemanticsNode semanticsNode = flutterSemanticsTree.get(virtualViewId); 502 if (semanticsNode == null) { 503 return null; 504 } 505 506 if (semanticsNode.platformViewId != -1) { 507 // For platform views we delegate the node creation to the accessibility view embedder. 508 View embeddedView = platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId); 509 Rect bounds = semanticsNode.getGlobalRect(); 510 return accessibilityViewEmbedder.getRootNode(embeddedView, semanticsNode.id, bounds); 511 } 512 513 AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(rootAccessibilityView, virtualViewId); 514 // Work around for https://github.com/flutter/flutter/issues/2101 515 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { 516 result.setViewIdResourceName(""); 517 } 518 result.setPackageName(rootAccessibilityView.getContext().getPackageName()); 519 result.setClassName("android.view.View"); 520 result.setSource(rootAccessibilityView, virtualViewId); 521 result.setFocusable(semanticsNode.isFocusable()); 522 if (inputFocusedSemanticsNode != null) { 523 result.setFocused(inputFocusedSemanticsNode.id == virtualViewId); 524 } 525 526 if (accessibilityFocusedSemanticsNode != null) { 527 result.setAccessibilityFocused(accessibilityFocusedSemanticsNode.id == virtualViewId); 528 } 529 530 if (semanticsNode.hasFlag(Flag.IS_TEXT_FIELD)) { 531 result.setPassword(semanticsNode.hasFlag(Flag.IS_OBSCURED)); 532 if (!semanticsNode.hasFlag(Flag.IS_READ_ONLY)) { 533 result.setClassName("android.widget.EditText"); 534 } 535 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { 536 result.setEditable(!semanticsNode.hasFlag(Flag.IS_READ_ONLY)); 537 if (semanticsNode.textSelectionBase != -1 && semanticsNode.textSelectionExtent != -1) { 538 result.setTextSelection(semanticsNode.textSelectionBase, semanticsNode.textSelectionExtent); 539 } 540 // Text fields will always be created as a live region when they have input focus, 541 // so that updates to the label trigger polite announcements. This makes it easy to 542 // follow a11y guidelines for text fields on Android. 543 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 && accessibilityFocusedSemanticsNode != null && accessibilityFocusedSemanticsNode.id == virtualViewId) { 544 result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); 545 } 546 } 547 548 // Cursor movements 549 int granularities = 0; 550 if (semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) { 551 result.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY); 552 granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER; 553 } 554 if (semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) { 555 result.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); 556 granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER; 557 } 558 if (semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_WORD)) { 559 result.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY); 560 granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD; 561 } 562 if (semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_WORD)) { 563 result.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); 564 granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD; 565 } 566 result.setMovementGranularities(granularities); 567 } 568 569 // These are non-ops on older devices. Attempting to interact with the text will cause Talkback to read the 570 // contents of the text box instead. 571 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { 572 if (semanticsNode.hasAction(Action.SET_SELECTION)) { 573 result.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION); 574 } 575 if (semanticsNode.hasAction(Action.COPY)) { 576 result.addAction(AccessibilityNodeInfo.ACTION_COPY); 577 } 578 if (semanticsNode.hasAction(Action.CUT)) { 579 result.addAction(AccessibilityNodeInfo.ACTION_CUT); 580 } 581 if (semanticsNode.hasAction(Action.PASTE)) { 582 result.addAction(AccessibilityNodeInfo.ACTION_PASTE); 583 } 584 } 585 586 if (semanticsNode.hasFlag(Flag.IS_BUTTON)) { 587 result.setClassName("android.widget.Button"); 588 } 589 if (semanticsNode.hasFlag(Flag.IS_IMAGE)) { 590 result.setClassName("android.widget.ImageView"); 591 // TODO(jonahwilliams): Figure out a way conform to the expected id from TalkBack's 592 // CustomLabelManager. talkback/src/main/java/labeling/CustomLabelManager.java#L525 593 } 594 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 && semanticsNode.hasAction(Action.DISMISS)) { 595 result.setDismissable(true); 596 result.addAction(AccessibilityNodeInfo.ACTION_DISMISS); 597 } 598 599 if (semanticsNode.parent != null) { 600 if (BuildConfig.DEBUG && semanticsNode.id <= ROOT_NODE_ID) { 601 Log.e(TAG, "Semantics node id is not > ROOT_NODE_ID."); 602 } 603 result.setParent(rootAccessibilityView, semanticsNode.parent.id); 604 } else { 605 if (BuildConfig.DEBUG && semanticsNode.id != ROOT_NODE_ID) { 606 Log.e(TAG, "Semantics node id does not equal ROOT_NODE_ID."); 607 } 608 result.setParent(rootAccessibilityView); 609 } 610 611 Rect bounds = semanticsNode.getGlobalRect(); 612 if (semanticsNode.parent != null) { 613 Rect parentBounds = semanticsNode.parent.getGlobalRect(); 614 Rect boundsInParent = new Rect(bounds); 615 boundsInParent.offset(-parentBounds.left, -parentBounds.top); 616 result.setBoundsInParent(boundsInParent); 617 } else { 618 result.setBoundsInParent(bounds); 619 } 620 result.setBoundsInScreen(bounds); 621 result.setVisibleToUser(true); 622 result.setEnabled( 623 !semanticsNode.hasFlag(Flag.HAS_ENABLED_STATE) || semanticsNode.hasFlag(Flag.IS_ENABLED) 624 ); 625 626 if (semanticsNode.hasAction(Action.TAP)) { 627 if (Build.VERSION.SDK_INT >= 21 && semanticsNode.onTapOverride != null) { 628 result.addAction(new AccessibilityNodeInfo.AccessibilityAction( 629 AccessibilityNodeInfo.ACTION_CLICK, 630 semanticsNode.onTapOverride.hint 631 )); 632 result.setClickable(true); 633 } else { 634 result.addAction(AccessibilityNodeInfo.ACTION_CLICK); 635 result.setClickable(true); 636 } 637 } 638 if (semanticsNode.hasAction(Action.LONG_PRESS)) { 639 if (Build.VERSION.SDK_INT >= 21 && semanticsNode.onLongPressOverride != null) { 640 result.addAction(new AccessibilityNodeInfo.AccessibilityAction( 641 AccessibilityNodeInfo.ACTION_LONG_CLICK, 642 semanticsNode.onLongPressOverride.hint 643 )); 644 result.setLongClickable(true); 645 } else { 646 result.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK); 647 result.setLongClickable(true); 648 } 649 } 650 if (semanticsNode.hasAction(Action.SCROLL_LEFT) || semanticsNode.hasAction(Action.SCROLL_UP) 651 || semanticsNode.hasAction(Action.SCROLL_RIGHT) || semanticsNode.hasAction(Action.SCROLL_DOWN)) { 652 result.setScrollable(true); 653 654 // This tells Android's a11y to send scroll events when reaching the end of 655 // the visible viewport of a scrollable, unless the node itself does not 656 // allow implicit scrolling - then we leave the className as view.View. 657 // 658 // We should prefer setCollectionInfo to the class names, as this way we get "In List" 659 // and "Out of list" announcements. But we don't always know the counts, so we 660 // can fallback to the generic scroll view class names. 661 // 662 // On older APIs, we always fall back to the generic scroll view class names here. 663 // 664 // TODO(dnfield): We should add semantics properties for rows and columns in 2 dimensional lists, e.g. 665 // GridView. Right now, we're only supporting ListViews and only if they have scroll children. 666 if (semanticsNode.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)) { 667 if (semanticsNode.hasAction(Action.SCROLL_LEFT) || semanticsNode.hasAction(Action.SCROLL_RIGHT)) { 668 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT && shouldSetCollectionInfo(semanticsNode)) { 669 result.setCollectionInfo(AccessibilityNodeInfo.CollectionInfo.obtain( 670 0, // rows 671 semanticsNode.scrollChildren, // columns 672 false // hierarchical 673 )); 674 } else { 675 result.setClassName("android.widget.HorizontalScrollView"); 676 } 677 } else { 678 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2 && shouldSetCollectionInfo(semanticsNode)) { 679 result.setCollectionInfo(AccessibilityNodeInfo.CollectionInfo.obtain( 680 semanticsNode.scrollChildren, // rows 681 0, // columns 682 false // hierarchical 683 )); 684 } else { 685 result.setClassName("android.widget.ScrollView"); 686 } 687 } 688 } 689 // TODO(ianh): Once we're on SDK v23+, call addAction to 690 // expose AccessibilityAction.ACTION_SCROLL_LEFT, _RIGHT, 691 // _UP, and _DOWN when appropriate. 692 if (semanticsNode.hasAction(Action.SCROLL_LEFT) || semanticsNode.hasAction(Action.SCROLL_UP)) { 693 result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 694 } 695 if (semanticsNode.hasAction(Action.SCROLL_RIGHT) || semanticsNode.hasAction(Action.SCROLL_DOWN)) { 696 result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 697 } 698 } 699 if (semanticsNode.hasAction(Action.INCREASE) || semanticsNode.hasAction(Action.DECREASE)) { 700 // TODO(jonahwilliams): support AccessibilityAction.ACTION_SET_PROGRESS once SDK is 701 // updated. 702 result.setClassName("android.widget.SeekBar"); 703 if (semanticsNode.hasAction(Action.INCREASE)) { 704 result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); 705 } 706 if (semanticsNode.hasAction(Action.DECREASE)) { 707 result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); 708 } 709 } 710 if (semanticsNode.hasFlag(Flag.IS_LIVE_REGION) && Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { 711 result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); 712 } 713 714 boolean hasCheckedState = semanticsNode.hasFlag(Flag.HAS_CHECKED_STATE); 715 boolean hasToggledState = semanticsNode.hasFlag(Flag.HAS_TOGGLED_STATE); 716 if (BuildConfig.DEBUG && (hasCheckedState && hasToggledState)) { 717 Log.e(TAG, "Expected semanticsNode to have checked state and toggled state."); 718 } 719 result.setCheckable(hasCheckedState || hasToggledState); 720 if (hasCheckedState) { 721 result.setChecked(semanticsNode.hasFlag(Flag.IS_CHECKED)); 722 result.setContentDescription(semanticsNode.getValueLabelHint()); 723 if (semanticsNode.hasFlag(Flag.IS_IN_MUTUALLY_EXCLUSIVE_GROUP)) { 724 result.setClassName("android.widget.RadioButton"); 725 } else { 726 result.setClassName("android.widget.CheckBox"); 727 } 728 } else if (hasToggledState) { 729 result.setChecked(semanticsNode.hasFlag(Flag.IS_TOGGLED)); 730 result.setClassName("android.widget.Switch"); 731 result.setContentDescription(semanticsNode.getValueLabelHint()); 732 } else { 733 // Setting the text directly instead of the content description 734 // will replace the "checked" or "not-checked" label. 735 result.setText(semanticsNode.getValueLabelHint()); 736 } 737 738 result.setSelected(semanticsNode.hasFlag(Flag.IS_SELECTED)); 739 740 // Accessibility Focus 741 if (accessibilityFocusedSemanticsNode != null && accessibilityFocusedSemanticsNode.id == virtualViewId) { 742 result.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); 743 } else { 744 result.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); 745 } 746 747 // Actions on the local context menu 748 if (Build.VERSION.SDK_INT >= 21) { 749 if (semanticsNode.customAccessibilityActions != null) { 750 for (CustomAccessibilityAction action : semanticsNode.customAccessibilityActions) { 751 result.addAction(new AccessibilityNodeInfo.AccessibilityAction( 752 action.resourceId, 753 action.label 754 )); 755 } 756 } 757 } 758 759 for (SemanticsNode child : semanticsNode.childrenInTraversalOrder) { 760 if (!child.hasFlag(Flag.IS_HIDDEN)) { 761 result.addChild(rootAccessibilityView, child.id); 762 } 763 } 764 765 return result; 766 } 767 768 /** 769 * Instructs the view represented by {@code virtualViewId} to carry out the desired {@code accessibilityAction}, 770 * perhaps configured by additional {@code arguments}. 771 * 772 * This method is invoked by Android's accessibility system. This method returns true if the 773 * desired {@code SemanticsNode} was found and was capable of performing the desired action, 774 * false otherwise. 775 * 776 * In a traditional Android app, the given view ID refers to a {@link View} within an Android 777 * {@link View} hierarchy. Flutter does not have an Android {@link View} hierarchy, therefore 778 * the given view ID is a {@code virtualViewId} that refers to a {@code SemanticsNode} within 779 * a Flutter app. The given arguments of this method are forwarded from Android to Flutter. 780 */ 781 @Override performAction(int virtualViewId, int accessibilityAction, @Nullable Bundle arguments)782 public boolean performAction(int virtualViewId, int accessibilityAction, @Nullable Bundle arguments) { 783 if (virtualViewId >= MIN_ENGINE_GENERATED_NODE_ID) { 784 // The node is in the engine generated range, and is handled by the accessibility view embedder. 785 boolean didPerform = accessibilityViewEmbedder.performAction(virtualViewId, accessibilityAction, arguments); 786 if (didPerform && accessibilityAction == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) { 787 embeddedAccessibilityFocusedNodeId = null; 788 } 789 return didPerform; 790 } 791 SemanticsNode semanticsNode = flutterSemanticsTree.get(virtualViewId); 792 if (semanticsNode == null) { 793 return false; 794 } 795 switch (accessibilityAction) { 796 case AccessibilityNodeInfo.ACTION_CLICK: { 797 // Note: TalkBack prior to Oreo doesn't use this handler and instead simulates a 798 // click event at the center of the SemanticsNode. Other a11y services might go 799 // through this handler though. 800 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.TAP); 801 return true; 802 } 803 case AccessibilityNodeInfo.ACTION_LONG_CLICK: { 804 // Note: TalkBack doesn't use this handler and instead simulates a long click event 805 // at the center of the SemanticsNode. Other a11y services might go through this 806 // handler though. 807 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.LONG_PRESS); 808 return true; 809 } 810 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { 811 if (semanticsNode.hasAction(Action.SCROLL_UP)) { 812 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.SCROLL_UP); 813 } else if (semanticsNode.hasAction(Action.SCROLL_LEFT)) { 814 // TODO(ianh): bidi support using textDirection 815 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.SCROLL_LEFT); 816 } else if (semanticsNode.hasAction(Action.INCREASE)) { 817 semanticsNode.value = semanticsNode.increasedValue; 818 // Event causes Android to read out the updated value. 819 sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED); 820 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.INCREASE); 821 } else { 822 return false; 823 } 824 return true; 825 } 826 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { 827 if (semanticsNode.hasAction(Action.SCROLL_DOWN)) { 828 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.SCROLL_DOWN); 829 } else if (semanticsNode.hasAction(Action.SCROLL_RIGHT)) { 830 // TODO(ianh): bidi support using textDirection 831 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.SCROLL_RIGHT); 832 } else if (semanticsNode.hasAction(Action.DECREASE)) { 833 semanticsNode.value = semanticsNode.decreasedValue; 834 // Event causes Android to read out the updated value. 835 sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED); 836 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.DECREASE); 837 } else { 838 return false; 839 } 840 return true; 841 } 842 case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: { 843 // Text selection APIs aren't available until API 18. We can't handle the case here so return false 844 // instead. It's extremely unlikely that this case would ever be triggered in the first place in API < 845 // 18. 846 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { 847 return false; 848 } 849 return performCursorMoveAction(semanticsNode, virtualViewId, arguments, false); 850 } 851 case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: { 852 // Text selection APIs aren't available until API 18. We can't handle the case here so return false 853 // instead. It's extremely unlikely that this case would ever be triggered in the first place in API < 854 // 18. 855 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { 856 return false; 857 } 858 return performCursorMoveAction(semanticsNode, virtualViewId, arguments, true); 859 } 860 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { 861 accessibilityChannel.dispatchSemanticsAction( 862 virtualViewId, 863 Action.DID_LOSE_ACCESSIBILITY_FOCUS 864 ); 865 sendAccessibilityEvent( 866 virtualViewId, 867 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED 868 ); 869 accessibilityFocusedSemanticsNode = null; 870 embeddedAccessibilityFocusedNodeId = null; 871 return true; 872 } 873 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { 874 accessibilityChannel.dispatchSemanticsAction( 875 virtualViewId, 876 Action.DID_GAIN_ACCESSIBILITY_FOCUS 877 ); 878 sendAccessibilityEvent( 879 virtualViewId, 880 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED 881 ); 882 883 if (accessibilityFocusedSemanticsNode == null) { 884 // When Android focuses a node, it doesn't invalidate the view. 885 // (It does when it sends ACTION_CLEAR_ACCESSIBILITY_FOCUS, so 886 // we only have to worry about this when the focused node is null.) 887 rootAccessibilityView.invalidate(); 888 } 889 accessibilityFocusedSemanticsNode = semanticsNode; 890 891 if (semanticsNode.hasAction(Action.INCREASE) || semanticsNode.hasAction(Action.DECREASE)) { 892 // SeekBars only announce themselves after this event. 893 sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED); 894 } 895 896 return true; 897 } 898 case ACTION_SHOW_ON_SCREEN: { 899 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.SHOW_ON_SCREEN); 900 return true; 901 } 902 case AccessibilityNodeInfo.ACTION_SET_SELECTION: { 903 // Text selection APIs aren't available until API 18. We can't handle the case here so return false 904 // instead. It's extremely unlikely that this case would ever be triggered in the first place in API < 905 // 18. 906 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { 907 return false; 908 } 909 final Map<String, Integer> selection = new HashMap<>(); 910 final boolean hasSelection = arguments != null 911 && arguments.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT) 912 && arguments.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT); 913 if (hasSelection) { 914 selection.put( 915 "base", 916 arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT) 917 ); 918 selection.put( 919 "extent", 920 arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT) 921 ); 922 } else { 923 // Clear the selection 924 selection.put("base", semanticsNode.textSelectionExtent); 925 selection.put("extent", semanticsNode.textSelectionExtent); 926 } 927 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.SET_SELECTION, selection); 928 return true; 929 } 930 case AccessibilityNodeInfo.ACTION_COPY: { 931 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.COPY); 932 return true; 933 } 934 case AccessibilityNodeInfo.ACTION_CUT: { 935 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.CUT); 936 return true; 937 } 938 case AccessibilityNodeInfo.ACTION_PASTE: { 939 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.PASTE); 940 return true; 941 } 942 case AccessibilityNodeInfo.ACTION_DISMISS: { 943 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.DISMISS); 944 return true; 945 } 946 default: 947 // might be a custom accessibility accessibilityAction. 948 final int flutterId = accessibilityAction - FIRST_RESOURCE_ID; 949 CustomAccessibilityAction contextAction = customAccessibilityActions.get(flutterId); 950 if (contextAction != null) { 951 accessibilityChannel.dispatchSemanticsAction( 952 virtualViewId, 953 Action.CUSTOM_ACTION, 954 contextAction.id 955 ); 956 return true; 957 } 958 } 959 return false; 960 } 961 962 /** 963 * Handles the responsibilities of {@link #performAction(int, int, Bundle)} for the specific 964 * scenario of cursor movement. 965 */ 966 @TargetApi(18) 967 @RequiresApi(18) performCursorMoveAction( @onNull SemanticsNode semanticsNode, int virtualViewId, @NonNull Bundle arguments, boolean forward )968 private boolean performCursorMoveAction( 969 @NonNull SemanticsNode semanticsNode, 970 int virtualViewId, 971 @NonNull Bundle arguments, 972 boolean forward 973 ) { 974 final int granularity = arguments.getInt( 975 AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT 976 ); 977 final boolean extendSelection = arguments.getBoolean( 978 AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN 979 ); 980 switch (granularity) { 981 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER: { 982 if (forward && semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) { 983 accessibilityChannel.dispatchSemanticsAction( 984 virtualViewId, 985 Action.MOVE_CURSOR_FORWARD_BY_CHARACTER, 986 extendSelection 987 ); 988 return true; 989 } 990 if (!forward && semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) { 991 accessibilityChannel.dispatchSemanticsAction( 992 virtualViewId, 993 Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER, 994 extendSelection 995 ); 996 return true; 997 } 998 break; 999 } 1000 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD: 1001 if (forward && semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_WORD)) { 1002 accessibilityChannel.dispatchSemanticsAction( 1003 virtualViewId, 1004 Action.MOVE_CURSOR_FORWARD_BY_WORD, 1005 extendSelection 1006 ); 1007 return true; 1008 } 1009 if (!forward && semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_WORD)) { 1010 accessibilityChannel.dispatchSemanticsAction( 1011 virtualViewId, 1012 Action.MOVE_CURSOR_BACKWARD_BY_WORD, 1013 extendSelection 1014 ); 1015 return true; 1016 } 1017 break; 1018 } 1019 return false; 1020 } 1021 1022 // TODO(ianh): implement findAccessibilityNodeInfosByText() 1023 1024 /** 1025 * Finds the view in a hierarchy that currently has the given type of {@code focus}. 1026 * 1027 * This method is invoked by Android's accessibility system. 1028 * 1029 * Flutter does not have an Android {@link View} hierarchy. Therefore, Flutter conceptually 1030 * handles this request by searching its semantics tree for the given {@code focus}, represented 1031 * by {@link #flutterSemanticsTree}. In practice, this {@code AccessibilityBridge} always 1032 * caches any active {@link #accessibilityFocusedSemanticsNode} and {@link #inputFocusedSemanticsNode}. 1033 * Therefore, no searching is necessary. This method directly inspects the given {@code focus} 1034 * type to return one of the cached nodes, null if the cached node is null, or null if a different 1035 * {@code focus} type is requested. 1036 */ 1037 @Override findFocus(int focus)1038 public AccessibilityNodeInfo findFocus(int focus) { 1039 switch (focus) { 1040 case AccessibilityNodeInfo.FOCUS_INPUT: { 1041 if (inputFocusedSemanticsNode != null) { 1042 return createAccessibilityNodeInfo(inputFocusedSemanticsNode.id); 1043 } 1044 if (embeddedInputFocusedNodeId != null) { 1045 return createAccessibilityNodeInfo(embeddedInputFocusedNodeId); 1046 } 1047 } 1048 // Fall through to check FOCUS_ACCESSIBILITY 1049 case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: { 1050 if (accessibilityFocusedSemanticsNode != null) { 1051 return createAccessibilityNodeInfo(accessibilityFocusedSemanticsNode.id); 1052 } 1053 if (embeddedAccessibilityFocusedNodeId != null) { 1054 return createAccessibilityNodeInfo(embeddedAccessibilityFocusedNodeId); 1055 } 1056 } 1057 } 1058 return null; 1059 } 1060 1061 /** 1062 * Returns the {@link SemanticsNode} at the root of Flutter's semantics tree. 1063 */ getRootSemanticsNode()1064 private SemanticsNode getRootSemanticsNode() { 1065 if (BuildConfig.DEBUG && !flutterSemanticsTree.containsKey(0)) { 1066 Log.e(TAG, "Attempted to getRootSemanticsNode without a root sematnics node."); 1067 } 1068 return flutterSemanticsTree.get(0); 1069 } 1070 1071 /** 1072 * Returns an existing {@link SemanticsNode} with the given {@code id}, if it exists within 1073 * {@link #flutterSemanticsTree}, or creates and returns a new {@link SemanticsNode} with the 1074 * given {@code id}, adding the new {@link SemanticsNode} to the {@link #flutterSemanticsTree}. 1075 * 1076 * This method should only be invoked as a result of receiving new information from Flutter. 1077 * The {@link #flutterSemanticsTree} is an Android cache of the last known state of a Flutter 1078 * app's semantics tree, therefore, invoking this method in any other situation will result in 1079 * a corrupt cache of Flutter's semantics tree. 1080 */ getOrCreateSemanticsNode(int id)1081 private SemanticsNode getOrCreateSemanticsNode(int id) { 1082 SemanticsNode semanticsNode = flutterSemanticsTree.get(id); 1083 if (semanticsNode == null) { 1084 semanticsNode = new SemanticsNode(this); 1085 semanticsNode.id = id; 1086 flutterSemanticsTree.put(id, semanticsNode); 1087 } 1088 return semanticsNode; 1089 } 1090 1091 /** 1092 * Returns an existing {@link CustomAccessibilityAction} with the given {@code id}, if it exists 1093 * within {@link #customAccessibilityActions}, or creates and returns a new {@link CustomAccessibilityAction} 1094 * with the given {@code id}, adding the new {@link CustomAccessibilityAction} to the 1095 * {@link #customAccessibilityActions}. 1096 * 1097 * This method should only be invoked as a result of receiving new information from Flutter. 1098 * The {@link #customAccessibilityActions} is an Android cache of the last known state of a Flutter 1099 * app's registered custom accessibility actions, therefore, invoking this method in any other 1100 * situation will result in a corrupt cache of Flutter's accessibility actions. 1101 */ getOrCreateAccessibilityAction(int id)1102 private CustomAccessibilityAction getOrCreateAccessibilityAction(int id) { 1103 CustomAccessibilityAction action = customAccessibilityActions.get(id); 1104 if (action == null) { 1105 action = new CustomAccessibilityAction(); 1106 action.id = id; 1107 action.resourceId = id + FIRST_RESOURCE_ID; 1108 customAccessibilityActions.put(id, action); 1109 } 1110 return action; 1111 } 1112 1113 /** 1114 * A hover {@link MotionEvent} has occurred in the {@code View} that corresponds to this 1115 * {@code AccessibilityBridge}. 1116 * 1117 * This method returns true if Flutter's accessibility system handled the hover event, false 1118 * otherwise. 1119 * 1120 * This method should be invoked from the corresponding {@code View}'s 1121 * {@link View#onHoverEvent(MotionEvent)}. 1122 */ onAccessibilityHoverEvent(MotionEvent event)1123 public boolean onAccessibilityHoverEvent(MotionEvent event) { 1124 if (!accessibilityManager.isTouchExplorationEnabled()) { 1125 return false; 1126 } 1127 1128 SemanticsNode semanticsNodeUnderCursor = getRootSemanticsNode().hitTest(new float[] {event.getX(), event.getY(), 0, 1}); 1129 if (semanticsNodeUnderCursor.platformViewId != -1) { 1130 return accessibilityViewEmbedder.onAccessibilityHoverEvent(semanticsNodeUnderCursor.id, event); 1131 } 1132 1133 if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER || event.getAction() == MotionEvent.ACTION_HOVER_MOVE) { 1134 handleTouchExploration(event.getX(), event.getY()); 1135 } else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) { 1136 onTouchExplorationExit(); 1137 } else { 1138 Log.d("flutter", "unexpected accessibility hover event: " + event); 1139 return false; 1140 } 1141 return true; 1142 } 1143 1144 /** 1145 * This method should be invoked when a hover interaction has the cursor move off of a 1146 * {@code SemanticsNode}. 1147 * 1148 * This method informs the Android accessibility system that a {@link AccessibilityEvent#TYPE_VIEW_HOVER_EXIT} 1149 * has occurred. 1150 */ onTouchExplorationExit()1151 private void onTouchExplorationExit() { 1152 if (hoveredObject != null) { 1153 sendAccessibilityEvent(hoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 1154 hoveredObject = null; 1155 } 1156 } 1157 1158 /** 1159 * This method should be invoked when a new hover interaction begins with a {@code SemanticsNode}, 1160 * or when an existing hover interaction sees a movement of the cursor. 1161 * 1162 * This method checks to see if the cursor has moved from one {@code SemanticsNode} to another. 1163 * If it has, this method informs the Android accessibility system of the change by first sending 1164 * a {@link AccessibilityEvent#TYPE_VIEW_HOVER_ENTER} event for the new hover node, followed by 1165 * a {@link AccessibilityEvent#TYPE_VIEW_HOVER_EXIT} event for the old hover node. 1166 */ handleTouchExploration(float x, float y)1167 private void handleTouchExploration(float x, float y) { 1168 if (flutterSemanticsTree.isEmpty()) { 1169 return; 1170 } 1171 SemanticsNode semanticsNodeUnderCursor = getRootSemanticsNode().hitTest(new float[] {x, y, 0, 1}); 1172 if (semanticsNodeUnderCursor != hoveredObject) { 1173 // sending ENTER before EXIT is how Android wants it 1174 if (semanticsNodeUnderCursor != null) { 1175 sendAccessibilityEvent(semanticsNodeUnderCursor.id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); 1176 } 1177 if (hoveredObject != null) { 1178 sendAccessibilityEvent(hoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); 1179 } 1180 hoveredObject = semanticsNodeUnderCursor; 1181 } 1182 } 1183 1184 /** 1185 * Updates the Android cache of Flutter's currently registered custom accessibility actions. 1186 * 1187 * The buffer received here is encoded by PlatformViewAndroid::UpdateSemantics, and the 1188 * decode logic here must be kept in sync with that method's encoding logic. 1189 */ 1190 // TODO(mattcarroll): Consider introducing ability to delete custom actions because they can 1191 // probably come and go in Flutter, so we may want to reflect that here in 1192 // the Android cache as well. updateCustomAccessibilityActions(@onNull ByteBuffer buffer, @NonNull String[] strings)1193 void updateCustomAccessibilityActions(@NonNull ByteBuffer buffer, @NonNull String[] strings) { 1194 while (buffer.hasRemaining()) { 1195 int id = buffer.getInt(); 1196 CustomAccessibilityAction action = getOrCreateAccessibilityAction(id); 1197 action.overrideId = buffer.getInt(); 1198 int stringIndex = buffer.getInt(); 1199 action.label = stringIndex == -1 ? null : strings[stringIndex]; 1200 stringIndex = buffer.getInt(); 1201 action.hint = stringIndex == -1 ? null : strings[stringIndex]; 1202 } 1203 } 1204 1205 /** 1206 * Updates {@link #flutterSemanticsTree} to reflect the latest state of Flutter's semantics tree. 1207 * 1208 * The latest state of Flutter's semantics tree is encoded in the given {@code buffer}. The buffer 1209 * is encoded by PlatformViewAndroid::UpdateSemantics, and the decode logic must be kept in sync 1210 * with that method's encoding logic. 1211 */ updateSemantics(@onNull ByteBuffer buffer, @NonNull String[] strings)1212 void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) { 1213 ArrayList<SemanticsNode> updated = new ArrayList<>(); 1214 while (buffer.hasRemaining()) { 1215 int id = buffer.getInt(); 1216 SemanticsNode semanticsNode = getOrCreateSemanticsNode(id); 1217 semanticsNode.updateWith(buffer, strings); 1218 if (semanticsNode.hasFlag(Flag.IS_HIDDEN)) { 1219 continue; 1220 } 1221 if (semanticsNode.hasFlag(Flag.IS_FOCUSED)) { 1222 inputFocusedSemanticsNode = semanticsNode; 1223 } 1224 if (semanticsNode.hadPreviousConfig) { 1225 updated.add(semanticsNode); 1226 } 1227 } 1228 1229 Set<SemanticsNode> visitedObjects = new HashSet<>(); 1230 SemanticsNode rootObject = getRootSemanticsNode(); 1231 List<SemanticsNode> newRoutes = new ArrayList<>(); 1232 if (rootObject != null) { 1233 final float[] identity = new float[16]; 1234 Matrix.setIdentityM(identity, 0); 1235 // in android devices API 23 and above, the system nav bar can be placed on the left side 1236 // of the screen in landscape mode. We must handle the translation ourselves for the 1237 // a11y nodes. 1238 if (Build.VERSION.SDK_INT >= 23) { 1239 WindowInsets insets = rootAccessibilityView.getRootWindowInsets(); 1240 if (insets != null) { 1241 if (!lastLeftFrameInset.equals(insets.getSystemWindowInsetLeft())) { 1242 rootObject.globalGeometryDirty = true; 1243 rootObject.inverseTransformDirty = true; 1244 } 1245 lastLeftFrameInset = insets.getSystemWindowInsetLeft(); 1246 Matrix.translateM(identity, 0, lastLeftFrameInset, 0, 0); 1247 } 1248 } 1249 rootObject.updateRecursively(identity, visitedObjects, false); 1250 rootObject.collectRoutes(newRoutes); 1251 } 1252 1253 // Dispatch a TYPE_WINDOW_STATE_CHANGED event if the most recent route id changed from the 1254 // previously cached route id. 1255 SemanticsNode lastAdded = null; 1256 for (SemanticsNode semanticsNode : newRoutes) { 1257 if (!flutterNavigationStack.contains(semanticsNode.id)) { 1258 lastAdded = semanticsNode; 1259 } 1260 } 1261 if (lastAdded == null && newRoutes.size() > 0) { 1262 lastAdded = newRoutes.get(newRoutes.size() - 1); 1263 } 1264 if (lastAdded != null && lastAdded.id != previousRouteId) { 1265 previousRouteId = lastAdded.id; 1266 createAndSendWindowChangeEvent(lastAdded); 1267 } 1268 flutterNavigationStack.clear(); 1269 for (SemanticsNode semanticsNode : newRoutes) { 1270 flutterNavigationStack.add(semanticsNode.id); 1271 } 1272 1273 Iterator<Map.Entry<Integer, SemanticsNode>> it = flutterSemanticsTree.entrySet().iterator(); 1274 while (it.hasNext()) { 1275 Map.Entry<Integer, SemanticsNode> entry = it.next(); 1276 SemanticsNode object = entry.getValue(); 1277 if (!visitedObjects.contains(object)) { 1278 willRemoveSemanticsNode(object); 1279 it.remove(); 1280 } 1281 } 1282 1283 // TODO(goderbauer): Send this event only once (!) for changed subtrees, 1284 // see https://github.com/flutter/flutter/issues/14534 1285 sendAccessibilityEvent(0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 1286 1287 for (SemanticsNode object : updated) { 1288 if (object.didScroll()) { 1289 AccessibilityEvent event = 1290 obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_SCROLLED); 1291 1292 // Android doesn't support unbound scrolling. So we pretend there is a large 1293 // bound (SCROLL_EXTENT_FOR_INFINITY), which you can never reach. 1294 float position = object.scrollPosition; 1295 float max = object.scrollExtentMax; 1296 if (Float.isInfinite(object.scrollExtentMax)) { 1297 max = SCROLL_EXTENT_FOR_INFINITY; 1298 if (position > SCROLL_POSITION_CAP_FOR_INFINITY) { 1299 position = SCROLL_POSITION_CAP_FOR_INFINITY; 1300 } 1301 } 1302 if (Float.isInfinite(object.scrollExtentMin)) { 1303 max += SCROLL_EXTENT_FOR_INFINITY; 1304 if (position < -SCROLL_POSITION_CAP_FOR_INFINITY) { 1305 position = -SCROLL_POSITION_CAP_FOR_INFINITY; 1306 } 1307 position += SCROLL_EXTENT_FOR_INFINITY; 1308 } else { 1309 max -= object.scrollExtentMin; 1310 position -= object.scrollExtentMin; 1311 } 1312 1313 if (object.hadAction(Action.SCROLL_UP) || object.hadAction(Action.SCROLL_DOWN)) { 1314 event.setScrollY((int) position); 1315 event.setMaxScrollY((int) max); 1316 } else if (object.hadAction(Action.SCROLL_LEFT) 1317 || object.hadAction(Action.SCROLL_RIGHT)) { 1318 event.setScrollX((int) position); 1319 event.setMaxScrollX((int) max); 1320 } 1321 if (object.scrollChildren > 0) { 1322 // We don't need to add 1 to the scroll index because TalkBack does this automagically. 1323 event.setItemCount(object.scrollChildren); 1324 event.setFromIndex(object.scrollIndex); 1325 int visibleChildren = 0; 1326 // handle hidden children at the beginning and end of the list. 1327 for (SemanticsNode child : object.childrenInHitTestOrder) { 1328 if (!child.hasFlag(Flag.IS_HIDDEN)) { 1329 visibleChildren += 1; 1330 } 1331 } 1332 if (BuildConfig.DEBUG) { 1333 if (object.scrollIndex + visibleChildren > object.scrollChildren) { 1334 Log.e(TAG, "Scroll index is out of bounds."); 1335 } 1336 1337 if (object.childrenInHitTestOrder.isEmpty()) { 1338 Log.e(TAG, "Had scrollChildren but no childrenInHitTestOrder"); 1339 } 1340 } 1341 // The setToIndex should be the index of the last visible child. Because we counted all 1342 // children, including the first index we need to subtract one. 1343 // 1344 // [0, 1, 2, 3, 4, 5] 1345 // ^ ^ 1346 // In the example above where 0 is the first visible index and 2 is the last, we will 1347 // count 3 total visible children. We then subtract one to get the correct last visible 1348 // index of 2. 1349 event.setToIndex(object.scrollIndex + visibleChildren - 1); 1350 } 1351 sendAccessibilityEvent(event); 1352 } 1353 if (object.hasFlag(Flag.IS_LIVE_REGION)) { 1354 String label = object.label == null ? "" : object.label; 1355 String previousLabel = object.previousLabel == null ? "" : object.label; 1356 if (!label.equals(previousLabel) || !object.hadFlag(Flag.IS_LIVE_REGION)) { 1357 sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 1358 } 1359 } else if (object.hasFlag(Flag.IS_TEXT_FIELD) && object.didChangeLabel() 1360 && inputFocusedSemanticsNode != null && inputFocusedSemanticsNode.id == object.id) { 1361 // Text fields should announce when their label changes while focused. We use a live 1362 // region tag to do so, and this event triggers that update. 1363 sendAccessibilityEvent(object.id, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 1364 } 1365 if (accessibilityFocusedSemanticsNode != null && accessibilityFocusedSemanticsNode.id == object.id 1366 && !object.hadFlag(Flag.IS_SELECTED) && object.hasFlag(Flag.IS_SELECTED)) { 1367 AccessibilityEvent event = 1368 obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_SELECTED); 1369 event.getText().add(object.label); 1370 sendAccessibilityEvent(event); 1371 } 1372 if (inputFocusedSemanticsNode != null && inputFocusedSemanticsNode.id == object.id 1373 && object.hadFlag(Flag.IS_TEXT_FIELD) && object.hasFlag(Flag.IS_TEXT_FIELD) 1374 // If we have a TextField that has InputFocus, we should avoid announcing it if something 1375 // else we track has a11y focus. This needs to still work when, e.g., IME has a11y focus 1376 // or the "PASTE" popup is used though. 1377 // See more discussion at https://github.com/flutter/flutter/issues/23180 1378 && (accessibilityFocusedSemanticsNode == null || (accessibilityFocusedSemanticsNode.id == inputFocusedSemanticsNode.id))) { 1379 String oldValue = object.previousValue != null ? object.previousValue : ""; 1380 String newValue = object.value != null ? object.value : ""; 1381 AccessibilityEvent event = createTextChangedEvent(object.id, oldValue, newValue); 1382 if (event != null) { 1383 sendAccessibilityEvent(event); 1384 } 1385 1386 if (object.previousTextSelectionBase != object.textSelectionBase 1387 || object.previousTextSelectionExtent != object.textSelectionExtent) { 1388 AccessibilityEvent selectionEvent = obtainAccessibilityEvent( 1389 object.id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); 1390 selectionEvent.getText().add(newValue); 1391 selectionEvent.setFromIndex(object.textSelectionBase); 1392 selectionEvent.setToIndex(object.textSelectionExtent); 1393 selectionEvent.setItemCount(newValue.length()); 1394 sendAccessibilityEvent(selectionEvent); 1395 } 1396 } 1397 } 1398 } 1399 createTextChangedEvent(int id, String oldValue, String newValue)1400 private AccessibilityEvent createTextChangedEvent(int id, String oldValue, String newValue) { 1401 AccessibilityEvent e = 1402 obtainAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); 1403 e.setBeforeText(oldValue); 1404 e.getText().add(newValue); 1405 1406 int i; 1407 for (i = 0; i < oldValue.length() && i < newValue.length(); ++i) { 1408 if (oldValue.charAt(i) != newValue.charAt(i)) { 1409 break; 1410 } 1411 } 1412 if (i >= oldValue.length() && i >= newValue.length()) { 1413 return null; // Text did not change 1414 } 1415 int firstDifference = i; 1416 e.setFromIndex(firstDifference); 1417 1418 int oldIndex = oldValue.length() - 1; 1419 int newIndex = newValue.length() - 1; 1420 while (oldIndex >= firstDifference && newIndex >= firstDifference) { 1421 if (oldValue.charAt(oldIndex) != newValue.charAt(newIndex)) { 1422 break; 1423 } 1424 --oldIndex; 1425 --newIndex; 1426 } 1427 e.setRemovedCount(oldIndex - firstDifference + 1); 1428 e.setAddedCount(newIndex - firstDifference + 1); 1429 1430 return e; 1431 } 1432 1433 /** 1434 * Sends an accessibility event of the given {@code eventType} to Android's accessibility 1435 * system with the given {@code viewId} represented as the source of the event. 1436 * 1437 * The given {@code viewId} may either belong to {@link #rootAccessibilityView}, or any 1438 * Flutter {@link SemanticsNode}. 1439 */ sendAccessibilityEvent(int viewId, int eventType)1440 private void sendAccessibilityEvent(int viewId, int eventType) { 1441 if (!accessibilityManager.isEnabled()) { 1442 return; 1443 } 1444 if (viewId == ROOT_NODE_ID) { 1445 rootAccessibilityView.sendAccessibilityEvent(eventType); 1446 } else { 1447 sendAccessibilityEvent(obtainAccessibilityEvent(viewId, eventType)); 1448 } 1449 } 1450 1451 /** 1452 * Sends the given {@link AccessibilityEvent} to Android's accessibility system for a given 1453 * Flutter {@link SemanticsNode}. 1454 * 1455 * This method should only be called for a Flutter {@link SemanticsNode}, not a traditional 1456 * Android {@code View}, i.e., {@link #rootAccessibilityView}. 1457 */ sendAccessibilityEvent(@onNull AccessibilityEvent event)1458 private void sendAccessibilityEvent(@NonNull AccessibilityEvent event) { 1459 if (!accessibilityManager.isEnabled()) { 1460 return; 1461 } 1462 // TODO(mattcarroll): why are we explicitly talking to the root view's parent? 1463 rootAccessibilityView.getParent().requestSendAccessibilityEvent(rootAccessibilityView, event); 1464 } 1465 1466 /** 1467 * Factory method that creates a {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED} and sends 1468 * the event to Android's accessibility system. 1469 * 1470 * The given {@code route} should be a {@link SemanticsNode} that represents a navigation route 1471 * in the Flutter app. 1472 */ createAndSendWindowChangeEvent(@onNull SemanticsNode route)1473 private void createAndSendWindowChangeEvent(@NonNull SemanticsNode route) { 1474 AccessibilityEvent event = obtainAccessibilityEvent( 1475 route.id, 1476 AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED 1477 ); 1478 String routeName = route.getRouteName(); 1479 event.getText().add(routeName); 1480 sendAccessibilityEvent(event); 1481 } 1482 1483 /** 1484 * Factory method that creates a new {@link AccessibilityEvent} that is configured to represent 1485 * the Flutter {@link SemanticsNode} represented by the given {@code virtualViewId}, categorized 1486 * as the given {@code eventType}. 1487 * 1488 * This method should *only* be called for Flutter {@link SemanticsNode}s. It should *not* be 1489 * invoked to create an {@link AccessibilityEvent} for the {@link #rootAccessibilityView}. 1490 */ obtainAccessibilityEvent(int virtualViewId, int eventType)1491 private AccessibilityEvent obtainAccessibilityEvent(int virtualViewId, int eventType) { 1492 if (BuildConfig.DEBUG && virtualViewId == ROOT_NODE_ID) { 1493 Log.e(TAG, "VirtualView node must not be the root node."); 1494 } 1495 AccessibilityEvent event = AccessibilityEvent.obtain(eventType); 1496 event.setPackageName(rootAccessibilityView.getContext().getPackageName()); 1497 event.setSource(rootAccessibilityView, virtualViewId); 1498 return event; 1499 } 1500 1501 /** 1502 * Hook called just before a {@link SemanticsNode} is removed from the Android cache of Flutter's 1503 * semantics tree. 1504 */ willRemoveSemanticsNode(SemanticsNode semanticsNodeToBeRemoved)1505 private void willRemoveSemanticsNode(SemanticsNode semanticsNodeToBeRemoved) { 1506 if (BuildConfig.DEBUG) { 1507 if (!flutterSemanticsTree.containsKey(semanticsNodeToBeRemoved.id)) { 1508 Log.e(TAG, "Attempted to remove a node that is not in the tree."); 1509 } 1510 if (flutterSemanticsTree.get(semanticsNodeToBeRemoved.id) != semanticsNodeToBeRemoved) { 1511 Log.e(TAG, "Flutter semantics tree failed to get expected node when searching by id."); 1512 } 1513 } 1514 // TODO(mattcarroll): should parent be set to "null" here? Changing the parent seems like the 1515 // behavior of a method called "removeSemanticsNode()". The same is true 1516 // for null'ing accessibilityFocusedSemanticsNode, inputFocusedSemanticsNode, 1517 // and hoveredObject. Is this a hook method or a command? 1518 semanticsNodeToBeRemoved.parent = null; 1519 if (accessibilityFocusedSemanticsNode == semanticsNodeToBeRemoved) { 1520 sendAccessibilityEvent( 1521 accessibilityFocusedSemanticsNode.id, 1522 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED 1523 ); 1524 accessibilityFocusedSemanticsNode = null; 1525 } 1526 if (inputFocusedSemanticsNode == semanticsNodeToBeRemoved) { 1527 inputFocusedSemanticsNode = null; 1528 } 1529 if (hoveredObject == semanticsNodeToBeRemoved) { 1530 hoveredObject = null; 1531 } 1532 } 1533 1534 /** 1535 * Resets the {@code AccessibilityBridge}: 1536 * <ul> 1537 * <li>Clears {@link #flutterSemanticsTree}, the Android cache of Flutter's semantics tree</li> 1538 * <li>Releases focus on any active {@link #accessibilityFocusedSemanticsNode}</li> 1539 * <li>Clears any hovered {@code SemanticsNode}</li> 1540 * <li>Sends a {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} event</li> 1541 * </ul> 1542 */ 1543 // TODO(mattcarroll): under what conditions is this method expected to be invoked? reset()1544 public void reset() { 1545 flutterSemanticsTree.clear(); 1546 if (accessibilityFocusedSemanticsNode != null) { 1547 sendAccessibilityEvent( 1548 accessibilityFocusedSemanticsNode.id, 1549 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED 1550 ); 1551 } 1552 accessibilityFocusedSemanticsNode = null; 1553 hoveredObject = null; 1554 sendAccessibilityEvent(0, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 1555 } 1556 1557 /** 1558 * Listener that can be set on a {@link AccessibilityBridge}, which is invoked any time 1559 * accessibility is turned on/off, or touch exploration is turned on/off. 1560 */ 1561 public interface OnAccessibilityChangeListener { onAccessibilityChanged(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled)1562 void onAccessibilityChanged(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled); 1563 } 1564 1565 // Must match SemanticsActions in semantics.dart 1566 // https://github.com/flutter/engine/blob/master/lib/ui/semantics.dart 1567 public enum Action { 1568 TAP(1 << 0), 1569 LONG_PRESS(1 << 1), 1570 SCROLL_LEFT(1 << 2), 1571 SCROLL_RIGHT(1 << 3), 1572 SCROLL_UP(1 << 4), 1573 SCROLL_DOWN(1 << 5), 1574 INCREASE(1 << 6), 1575 DECREASE(1 << 7), 1576 SHOW_ON_SCREEN(1 << 8), 1577 MOVE_CURSOR_FORWARD_BY_CHARACTER(1 << 9), 1578 MOVE_CURSOR_BACKWARD_BY_CHARACTER(1 << 10), 1579 SET_SELECTION(1 << 11), 1580 COPY(1 << 12), 1581 CUT(1 << 13), 1582 PASTE(1 << 14), 1583 DID_GAIN_ACCESSIBILITY_FOCUS(1 << 15), 1584 DID_LOSE_ACCESSIBILITY_FOCUS(1 << 16), 1585 CUSTOM_ACTION(1 << 17), 1586 DISMISS(1 << 18), 1587 MOVE_CURSOR_FORWARD_BY_WORD(1 << 19), 1588 MOVE_CURSOR_BACKWARD_BY_WORD(1 << 20); 1589 1590 public final int value; 1591 Action(int value)1592 Action(int value) { 1593 this.value = value; 1594 } 1595 } 1596 1597 // Must match SemanticsFlag in semantics.dart 1598 // https://github.com/flutter/engine/blob/master/lib/ui/semantics.dart 1599 private enum Flag { 1600 HAS_CHECKED_STATE(1 << 0), 1601 IS_CHECKED(1 << 1), 1602 IS_SELECTED(1 << 2), 1603 IS_BUTTON(1 << 3), 1604 IS_TEXT_FIELD(1 << 4), 1605 IS_FOCUSED(1 << 5), 1606 HAS_ENABLED_STATE(1 << 6), 1607 IS_ENABLED(1 << 7), 1608 IS_IN_MUTUALLY_EXCLUSIVE_GROUP(1 << 8), 1609 IS_HEADER(1 << 9), 1610 IS_OBSCURED(1 << 10), 1611 SCOPES_ROUTE(1 << 11), 1612 NAMES_ROUTE(1 << 12), 1613 IS_HIDDEN(1 << 13), 1614 IS_IMAGE(1 << 14), 1615 IS_LIVE_REGION(1 << 15), 1616 HAS_TOGGLED_STATE(1 << 16), 1617 IS_TOGGLED(1 << 17), 1618 HAS_IMPLICIT_SCROLLING(1 << 18), 1619 // The Dart API defines the following flag but it isn't used in Android. 1620 // IS_MULTILINE(1 << 19); 1621 IS_READ_ONLY(1 << 20); 1622 1623 final int value; 1624 Flag(int value)1625 Flag(int value) { 1626 this.value = value; 1627 } 1628 } 1629 1630 // Must match the enum defined in window.dart. 1631 private enum AccessibilityFeature { 1632 ACCESSIBLE_NAVIGATION(1 << 0), 1633 INVERT_COLORS(1 << 1), // NOT SUPPORTED 1634 DISABLE_ANIMATIONS(1 << 2); 1635 1636 final int value; 1637 AccessibilityFeature(int value)1638 AccessibilityFeature(int value) { 1639 this.value = value; 1640 } 1641 } 1642 1643 private enum TextDirection { 1644 UNKNOWN, 1645 LTR, 1646 RTL; 1647 fromInt(int value)1648 public static TextDirection fromInt(int value) { 1649 switch (value) { 1650 case 1: 1651 return RTL; 1652 case 2: 1653 return LTR; 1654 } 1655 return UNKNOWN; 1656 } 1657 } 1658 1659 /** 1660 * Accessibility action that is defined within a given Flutter application, as opposed to the 1661 * standard accessibility actions that are available in the Flutter framework. 1662 * 1663 * Flutter and Android support a number of built-in accessibility actions. However, these 1664 * predefined actions are not always sufficient for a desired interaction. Android facilitates 1665 * custom accessibility actions, https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction. 1666 * Flutter supports custom accessibility actions via {@code customSemanticsActions} within 1667 * a {@code Semantics} widget, https://docs.flutter.io/flutter/widgets/Semantics-class.html. 1668 * 1669 * See the Android documentation for custom accessibility actions: 1670 * https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction 1671 * 1672 * See the Flutter documentation for the Semantics widget: 1673 * https://docs.flutter.io/flutter/widgets/Semantics-class.html 1674 */ 1675 private static class CustomAccessibilityAction { CustomAccessibilityAction()1676 CustomAccessibilityAction() {} 1677 1678 // The ID of the custom action plus a minimum value so that the identifier 1679 // does not collide with existing Android accessibility actions. This ID 1680 // represents and Android resource ID, not a Flutter ID. 1681 private int resourceId = -1; 1682 1683 // The Flutter ID of this custom accessibility action. See Flutter's Semantics widget for 1684 // custom accessibility action definitions: https://docs.flutter.io/flutter/widgets/Semantics-class.html 1685 private int id = -1; 1686 1687 // The ID of the standard Flutter accessibility action that this {@code CustomAccessibilityAction} 1688 // overrides with a custom {@code label} and/or {@code hint}. 1689 private int overrideId = -1; 1690 1691 // The user presented value which is displayed in the local context menu. 1692 private String label; 1693 1694 // The text used in overridden standard actions. 1695 private String hint; 1696 } 1697 1698 /** 1699 * Flutter {@code SemanticsNode} represented in Java/Android. 1700 * 1701 * Flutter maintains a semantics tree that is controlled by, but is independent of Flutter's 1702 * element tree, i.e., widgets/elements/render objects. Flutter's semantics tree must be cached 1703 * on the Android side so that Android can query any {@code SemanticsNode} at any time. This 1704 * class represents a single node in the semantics tree, and it is a Java representation of the 1705 * analogous concept within Flutter. 1706 * 1707 * To see how this {@code SemanticsNode}'s fields correspond to Flutter's semantics system, see 1708 * semantics.dart: https://github.com/flutter/engine/blob/master/lib/ui/semantics.dart 1709 */ 1710 private static class SemanticsNode { nullableHasAncestor(SemanticsNode target, Predicate<SemanticsNode> tester)1711 private static boolean nullableHasAncestor(SemanticsNode target, Predicate<SemanticsNode> tester) { 1712 return target != null && target.getAncestor(tester) != null; 1713 } 1714 1715 final AccessibilityBridge accessibilityBridge; 1716 1717 // Flutter ID of this {@code SemanticsNode}. 1718 private int id = -1; 1719 1720 private int flags; 1721 private int actions; 1722 private int textSelectionBase; 1723 private int textSelectionExtent; 1724 private int platformViewId; 1725 private int scrollChildren; 1726 private int scrollIndex; 1727 private float scrollPosition; 1728 private float scrollExtentMax; 1729 private float scrollExtentMin; 1730 private String label; 1731 private String value; 1732 private String increasedValue; 1733 private String decreasedValue; 1734 private String hint; 1735 1736 // See Flutter's {@code SemanticsNode#textDirection}. 1737 private TextDirection textDirection; 1738 1739 private boolean hadPreviousConfig = false; 1740 private int previousFlags; 1741 private int previousActions; 1742 private int previousTextSelectionBase; 1743 private int previousTextSelectionExtent; 1744 private float previousScrollPosition; 1745 private float previousScrollExtentMax; 1746 private float previousScrollExtentMin; 1747 private String previousValue; 1748 private String previousLabel; 1749 1750 private float left; 1751 private float top; 1752 private float right; 1753 private float bottom; 1754 private float[] transform; 1755 1756 private SemanticsNode parent; 1757 private List<SemanticsNode> childrenInTraversalOrder = new ArrayList<>(); 1758 private List<SemanticsNode> childrenInHitTestOrder = new ArrayList<>(); 1759 private List<CustomAccessibilityAction> customAccessibilityActions; 1760 private CustomAccessibilityAction onTapOverride; 1761 private CustomAccessibilityAction onLongPressOverride; 1762 1763 private boolean inverseTransformDirty = true; 1764 private float[] inverseTransform; 1765 1766 private boolean globalGeometryDirty = true; 1767 private float[] globalTransform; 1768 private Rect globalRect; 1769 SemanticsNode(@onNull AccessibilityBridge accessibilityBridge)1770 SemanticsNode(@NonNull AccessibilityBridge accessibilityBridge) { 1771 this.accessibilityBridge = accessibilityBridge; 1772 } 1773 1774 /** 1775 * Returns the ancestor of this {@code SemanticsNode} for which {@link Predicate#test(Object)} 1776 * returns true, or null if no such ancestor exists. 1777 */ getAncestor(Predicate<SemanticsNode> tester)1778 private SemanticsNode getAncestor(Predicate<SemanticsNode> tester) { 1779 SemanticsNode nextAncestor = parent; 1780 while (nextAncestor != null) { 1781 if (tester.test(nextAncestor)) { 1782 return nextAncestor; 1783 } 1784 nextAncestor = nextAncestor.parent; 1785 } 1786 return null; 1787 } 1788 1789 /** 1790 * Returns true if the given {@code action} is supported by this {@code SemanticsNode}. 1791 * 1792 * This method only applies to this {@code SemanticsNode} and does not implicitly search 1793 * its children. 1794 */ hasAction(@onNull Action action)1795 private boolean hasAction(@NonNull Action action) { 1796 return (actions & action.value) != 0; 1797 } 1798 1799 /** 1800 * Returns true if the given {@code action} was supported by the immediately previous 1801 * version of this {@code SemanticsNode}. 1802 */ hadAction(@onNull Action action)1803 private boolean hadAction(@NonNull Action action) { 1804 return (previousActions & action.value) != 0; 1805 } 1806 hasFlag(@onNull Flag flag)1807 private boolean hasFlag(@NonNull Flag flag) { 1808 return (flags & flag.value) != 0; 1809 } 1810 hadFlag(@onNull Flag flag)1811 private boolean hadFlag(@NonNull Flag flag) { 1812 if (BuildConfig.DEBUG && !hadPreviousConfig) { 1813 Log.e(TAG, "Attempted to check hadFlag but had no previous config."); 1814 } 1815 return (previousFlags & flag.value) != 0; 1816 } 1817 didScroll()1818 private boolean didScroll() { 1819 return !Float.isNaN(scrollPosition) && !Float.isNaN(previousScrollPosition) 1820 && previousScrollPosition != scrollPosition; 1821 } 1822 didChangeLabel()1823 private boolean didChangeLabel() { 1824 if (label == null && previousLabel == null) { 1825 return false; 1826 } 1827 return label == null || previousLabel == null || !label.equals(previousLabel); 1828 } 1829 log(@onNull String indent, boolean recursive)1830 private void log(@NonNull String indent, boolean recursive) { 1831 if (BuildConfig.DEBUG) { 1832 Log.i(TAG, 1833 indent + "SemanticsNode id=" + id + " label=" + label + " actions=" + actions 1834 + " flags=" + flags + "\n" + indent + " +-- textDirection=" 1835 + textDirection + "\n" + indent + " +-- rect.ltrb=(" + left + ", " 1836 + top + ", " + right + ", " + bottom + ")\n" + indent 1837 + " +-- transform=" + Arrays.toString(transform) + "\n"); 1838 if (recursive) { 1839 String childIndent = indent + " "; 1840 for (SemanticsNode child : childrenInTraversalOrder) { 1841 child.log(childIndent, recursive); 1842 } 1843 } 1844 } 1845 } 1846 updateWith(@onNull ByteBuffer buffer, @NonNull String[] strings)1847 private void updateWith(@NonNull ByteBuffer buffer, @NonNull String[] strings) { 1848 hadPreviousConfig = true; 1849 previousValue = value; 1850 previousLabel = label; 1851 previousFlags = flags; 1852 previousActions = actions; 1853 previousTextSelectionBase = textSelectionBase; 1854 previousTextSelectionExtent = textSelectionExtent; 1855 previousScrollPosition = scrollPosition; 1856 previousScrollExtentMax = scrollExtentMax; 1857 previousScrollExtentMin = scrollExtentMin; 1858 1859 flags = buffer.getInt(); 1860 actions = buffer.getInt(); 1861 textSelectionBase = buffer.getInt(); 1862 textSelectionExtent = buffer.getInt(); 1863 platformViewId = buffer.getInt(); 1864 scrollChildren = buffer.getInt(); 1865 scrollIndex = buffer.getInt(); 1866 scrollPosition = buffer.getFloat(); 1867 scrollExtentMax = buffer.getFloat(); 1868 scrollExtentMin = buffer.getFloat(); 1869 1870 int stringIndex = buffer.getInt(); 1871 label = stringIndex == -1 ? null : strings[stringIndex]; 1872 1873 stringIndex = buffer.getInt(); 1874 value = stringIndex == -1 ? null : strings[stringIndex]; 1875 1876 stringIndex = buffer.getInt(); 1877 increasedValue = stringIndex == -1 ? null : strings[stringIndex]; 1878 1879 stringIndex = buffer.getInt(); 1880 decreasedValue = stringIndex == -1 ? null : strings[stringIndex]; 1881 1882 stringIndex = buffer.getInt(); 1883 hint = stringIndex == -1 ? null : strings[stringIndex]; 1884 1885 textDirection = TextDirection.fromInt(buffer.getInt()); 1886 1887 left = buffer.getFloat(); 1888 top = buffer.getFloat(); 1889 right = buffer.getFloat(); 1890 bottom = buffer.getFloat(); 1891 1892 if (transform == null) { 1893 transform = new float[16]; 1894 } 1895 for (int i = 0; i < 16; ++i) { 1896 transform[i] = buffer.getFloat(); 1897 } 1898 inverseTransformDirty = true; 1899 globalGeometryDirty = true; 1900 1901 final int childCount = buffer.getInt(); 1902 childrenInTraversalOrder.clear(); 1903 childrenInHitTestOrder.clear(); 1904 for (int i = 0; i < childCount; ++i) { 1905 SemanticsNode child = accessibilityBridge.getOrCreateSemanticsNode(buffer.getInt()); 1906 child.parent = this; 1907 childrenInTraversalOrder.add(child); 1908 } 1909 for (int i = 0; i < childCount; ++i) { 1910 SemanticsNode child = accessibilityBridge.getOrCreateSemanticsNode(buffer.getInt()); 1911 child.parent = this; 1912 childrenInHitTestOrder.add(child); 1913 } 1914 1915 final int actionCount = buffer.getInt(); 1916 if (actionCount == 0) { 1917 customAccessibilityActions = null; 1918 } else { 1919 if (customAccessibilityActions == null) 1920 customAccessibilityActions = new ArrayList<>(actionCount); 1921 else 1922 customAccessibilityActions.clear(); 1923 1924 for (int i = 0; i < actionCount; i++) { 1925 CustomAccessibilityAction action = accessibilityBridge.getOrCreateAccessibilityAction(buffer.getInt()); 1926 if (action.overrideId == Action.TAP.value) { 1927 onTapOverride = action; 1928 } else if (action.overrideId == Action.LONG_PRESS.value) { 1929 onLongPressOverride = action; 1930 } else { 1931 // If we receive a different overrideId it means that we were passed 1932 // a standard action to override that we don't yet support. 1933 if (BuildConfig.DEBUG && action.overrideId != -1) { 1934 Log.e(TAG, "Expected action.overrideId to be -1."); 1935 } 1936 customAccessibilityActions.add(action); 1937 } 1938 customAccessibilityActions.add(action); 1939 } 1940 } 1941 } 1942 ensureInverseTransform()1943 private void ensureInverseTransform() { 1944 if (!inverseTransformDirty) { 1945 return; 1946 } 1947 inverseTransformDirty = false; 1948 if (inverseTransform == null) { 1949 inverseTransform = new float[16]; 1950 } 1951 if (!Matrix.invertM(inverseTransform, 0, transform, 0)) { 1952 Arrays.fill(inverseTransform, 0); 1953 } 1954 } 1955 getGlobalRect()1956 private Rect getGlobalRect() { 1957 if (BuildConfig.DEBUG && globalGeometryDirty) { 1958 Log.e(TAG, "Attempted to getGlobalRect with a dirty geometry."); 1959 } 1960 return globalRect; 1961 } 1962 hitTest(float[] point)1963 private SemanticsNode hitTest(float[] point) { 1964 final float w = point[3]; 1965 final float x = point[0] / w; 1966 final float y = point[1] / w; 1967 if (x < left || x >= right || y < top || y >= bottom) return null; 1968 final float[] transformedPoint = new float[4]; 1969 for (SemanticsNode child : childrenInHitTestOrder) { 1970 if (child.hasFlag(Flag.IS_HIDDEN)) { 1971 continue; 1972 } 1973 child.ensureInverseTransform(); 1974 Matrix.multiplyMV(transformedPoint, 0, child.inverseTransform, 0, point, 0); 1975 final SemanticsNode result = child.hitTest(transformedPoint); 1976 if (result != null) { 1977 return result; 1978 } 1979 } 1980 return this; 1981 } 1982 1983 // TODO(goderbauer): This should be decided by the framework once we have more information 1984 // about focusability there. isFocusable()1985 private boolean isFocusable() { 1986 // We enforce in the framework that no other useful semantics are merged with these 1987 // nodes. 1988 if (hasFlag(Flag.SCOPES_ROUTE)) { 1989 return false; 1990 } 1991 int scrollableActions = Action.SCROLL_RIGHT.value | Action.SCROLL_LEFT.value 1992 | Action.SCROLL_UP.value | Action.SCROLL_DOWN.value; 1993 return (actions & ~scrollableActions) != 0 || flags != 0 1994 || (label != null && !label.isEmpty()) || (value != null && !value.isEmpty()) 1995 || (hint != null && !hint.isEmpty()); 1996 } 1997 collectRoutes(List<SemanticsNode> edges)1998 private void collectRoutes(List<SemanticsNode> edges) { 1999 if (hasFlag(Flag.SCOPES_ROUTE)) { 2000 edges.add(this); 2001 } 2002 for (SemanticsNode child : childrenInTraversalOrder) { 2003 child.collectRoutes(edges); 2004 } 2005 } 2006 getRouteName()2007 private String getRouteName() { 2008 // Returns the first non-null and non-empty semantic label of a child 2009 // with an NamesRoute flag. Otherwise returns null. 2010 if (hasFlag(Flag.NAMES_ROUTE)) { 2011 if (label != null && !label.isEmpty()) { 2012 return label; 2013 } 2014 } 2015 for (SemanticsNode child : childrenInTraversalOrder) { 2016 String newName = child.getRouteName(); 2017 if (newName != null && !newName.isEmpty()) { 2018 return newName; 2019 } 2020 } 2021 return null; 2022 } 2023 updateRecursively(float[] ancestorTransform, Set<SemanticsNode> visitedObjects, boolean forceUpdate)2024 private void updateRecursively(float[] ancestorTransform, Set<SemanticsNode> visitedObjects, 2025 boolean forceUpdate) { 2026 visitedObjects.add(this); 2027 2028 if (globalGeometryDirty) { 2029 forceUpdate = true; 2030 } 2031 2032 if (forceUpdate) { 2033 if (globalTransform == null) { 2034 globalTransform = new float[16]; 2035 } 2036 Matrix.multiplyMM(globalTransform, 0, ancestorTransform, 0, transform, 0); 2037 2038 final float[] sample = new float[4]; 2039 sample[2] = 0; 2040 sample[3] = 1; 2041 2042 final float[] point1 = new float[4]; 2043 final float[] point2 = new float[4]; 2044 final float[] point3 = new float[4]; 2045 final float[] point4 = new float[4]; 2046 2047 sample[0] = left; 2048 sample[1] = top; 2049 transformPoint(point1, globalTransform, sample); 2050 2051 sample[0] = right; 2052 sample[1] = top; 2053 transformPoint(point2, globalTransform, sample); 2054 2055 sample[0] = right; 2056 sample[1] = bottom; 2057 transformPoint(point3, globalTransform, sample); 2058 2059 sample[0] = left; 2060 sample[1] = bottom; 2061 transformPoint(point4, globalTransform, sample); 2062 2063 if (globalRect == null) globalRect = new Rect(); 2064 2065 globalRect.set(Math.round(min(point1[0], point2[0], point3[0], point4[0])), 2066 Math.round(min(point1[1], point2[1], point3[1], point4[1])), 2067 Math.round(max(point1[0], point2[0], point3[0], point4[0])), 2068 Math.round(max(point1[1], point2[1], point3[1], point4[1]))); 2069 2070 globalGeometryDirty = false; 2071 } 2072 2073 if (BuildConfig.DEBUG) { 2074 if (globalTransform == null) { 2075 Log.e(TAG, "Expected globalTransform to not be null."); 2076 } 2077 if (globalRect == null) { 2078 Log.e(TAG, "Expected globalRect to not be null."); 2079 } 2080 } 2081 2082 for (SemanticsNode child : childrenInTraversalOrder) { 2083 child.updateRecursively(globalTransform, visitedObjects, forceUpdate); 2084 } 2085 } 2086 transformPoint(float[] result, float[] transform, float[] point)2087 private void transformPoint(float[] result, float[] transform, float[] point) { 2088 Matrix.multiplyMV(result, 0, transform, 0, point, 0); 2089 final float w = result[3]; 2090 result[0] /= w; 2091 result[1] /= w; 2092 result[2] /= w; 2093 result[3] = 0; 2094 } 2095 min(float a, float b, float c, float d)2096 private float min(float a, float b, float c, float d) { 2097 return Math.min(a, Math.min(b, Math.min(c, d))); 2098 } 2099 max(float a, float b, float c, float d)2100 private float max(float a, float b, float c, float d) { 2101 return Math.max(a, Math.max(b, Math.max(c, d))); 2102 } 2103 getValueLabelHint()2104 private String getValueLabelHint() { 2105 StringBuilder sb = new StringBuilder(); 2106 String[] array = {value, label, hint}; 2107 for (String word : array) { 2108 if (word != null && word.length() > 0) { 2109 if (sb.length() > 0) sb.append(", "); 2110 sb.append(word); 2111 } 2112 } 2113 return sb.length() > 0 ? sb.toString() : null; 2114 } 2115 } 2116 2117 /** 2118 * Delegates handling of {@link android.view.ViewParent#requestSendAccessibilityEvent} to the accessibility bridge. 2119 * 2120 * This is used by embedded platform views to propagate accessibility events from their view hierarchy to the 2121 * accessibility bridge. 2122 * 2123 * As the embedded view doesn't have to be the only View in the embedded hierarchy (it can have child views) and the 2124 * event might have been originated from any view in this hierarchy, this method gets both a reference to the 2125 * embedded platform view, and a reference to the view from its hierarchy that sent the event. 2126 * 2127 * @param embeddedView the embedded platform view for which the event is delegated 2128 * @param eventOrigin the view in the embedded view's hierarchy that sent the event. 2129 * @return True if the event was sent. 2130 */ externalViewRequestSendAccessibilityEvent(View embeddedView, View eventOrigin, AccessibilityEvent event)2131 public boolean externalViewRequestSendAccessibilityEvent(View embeddedView, View eventOrigin, AccessibilityEvent event) { 2132 if (!accessibilityViewEmbedder.requestSendAccessibilityEvent(embeddedView, eventOrigin, event)){ 2133 return false; 2134 } 2135 Integer virtualNodeId = accessibilityViewEmbedder.getRecordFlutterId(embeddedView, event); 2136 if (virtualNodeId == null) { 2137 return false; 2138 } 2139 switch(event.getEventType()) { 2140 case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: 2141 hoveredObject = null; 2142 break; 2143 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: 2144 embeddedAccessibilityFocusedNodeId = virtualNodeId; 2145 accessibilityFocusedSemanticsNode = null; 2146 break; 2147 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: 2148 embeddedInputFocusedNodeId = null; 2149 embeddedAccessibilityFocusedNodeId = null; 2150 break; 2151 case AccessibilityEvent.TYPE_VIEW_FOCUSED: 2152 embeddedInputFocusedNodeId = virtualNodeId; 2153 inputFocusedSemanticsNode = null; 2154 break; 2155 } 2156 return true; 2157 } 2158 } 2159