Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
AccessibilityBridge.java
Go to the documentation of this file.
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
5package io.flutter.view;
6
7import static io.flutter.Build.API_LEVELS;
8
9import android.annotation.SuppressLint;
10import android.annotation.TargetApi;
11import android.app.Activity;
12import android.content.ContentResolver;
13import android.content.Context;
14import android.content.res.Configuration;
15import android.database.ContentObserver;
16import android.graphics.Rect;
17import android.net.Uri;
18import android.opengl.Matrix;
19import android.os.Build;
20import android.os.Bundle;
21import android.os.Handler;
22import android.provider.Settings;
23import android.text.SpannableString;
24import android.text.TextUtils;
25import android.text.style.LocaleSpan;
26import android.text.style.TtsSpan;
27import android.view.MotionEvent;
28import android.view.View;
29import android.view.WindowInsets;
30import android.view.WindowManager;
31import android.view.accessibility.AccessibilityEvent;
32import android.view.accessibility.AccessibilityManager;
33import android.view.accessibility.AccessibilityNodeInfo;
34import android.view.accessibility.AccessibilityNodeProvider;
35import androidx.annotation.NonNull;
36import androidx.annotation.Nullable;
37import androidx.annotation.RequiresApi;
38import androidx.annotation.VisibleForTesting;
39import io.flutter.BuildConfig;
40import io.flutter.Log;
41import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
42import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate;
43import io.flutter.util.Predicate;
44import io.flutter.util.ViewUtils;
45import java.nio.ByteBuffer;
46import java.nio.ByteOrder;
47import java.nio.charset.Charset;
48import java.util.*;
49import java.util.regex.Matcher;
50import java.util.regex.Pattern;
51
52/**
53 * Bridge between Android's OS accessibility system and Flutter's accessibility system.
54 *
55 * <p>An {@code AccessibilityBridge} requires:
56 *
57 * <ul>
58 * <li>A real Android {@link View}, called the {@link #rootAccessibilityView}, which contains a
59 * Flutter UI. The {@link #rootAccessibilityView} is required at the time of {@code
60 * AccessibilityBridge}'s instantiation and is held for the duration of {@code
61 * AccessibilityBridge}'s lifespan. {@code AccessibilityBridge} invokes various accessibility
62 * methods on the {@link #rootAccessibilityView}, e.g., {@link
63 * View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo)}. The {@link
64 * #rootAccessibilityView} is expected to notify the {@code AccessibilityBridge} of relevant
65 * interactions: {@link #onAccessibilityHoverEvent(MotionEvent)}, {@link #reset()}, {@link
66 * #updateSemantics(ByteBuffer, String[], ByteBuffer[])}, and {@link
67 * #updateCustomAccessibilityActions(ByteBuffer, String[])}
68 * <li>An {@link AccessibilityChannel} that is connected to the running Flutter app.
69 * <li>Android's {@link AccessibilityManager} to query and listen for accessibility settings.
70 * <li>Android's {@link ContentResolver} to listen for changes to system animation settings.
71 * </ul>
72 *
73 * The {@code AccessibilityBridge} causes Android to treat Flutter {@code SemanticsNode}s as if they
74 * were accessible Android {@link View}s. Accessibility requests may be sent from a Flutter widget
75 * to the Android OS, as if it were an Android {@link View}, and accessibility events may be
76 * consumed by a Flutter widget, as if it were an Android {@link View}. {@code AccessibilityBridge}
77 * refers to Flutter's accessible widgets as "virtual views" and identifies them with "virtual view
78 * IDs".
79 */
80public class AccessibilityBridge extends AccessibilityNodeProvider {
81 private static final String TAG = "AccessibilityBridge";
82
83 // Constants from higher API levels.
84 // TODO(goderbauer): Get these from Android Support Library when
85 // https://github.com/flutter/flutter/issues/11099 is resolved.
86 private static final int ACTION_SHOW_ON_SCREEN = 16908342; // API level 23
87
88 private static final float SCROLL_EXTENT_FOR_INFINITY = 100000.0f;
89 private static final float SCROLL_POSITION_CAP_FOR_INFINITY = 70000.0f;
90 private static final int ROOT_NODE_ID = 0;
91 private static final int SCROLLABLE_ACTIONS =
92 Action.SCROLL_RIGHT.value
93 | Action.SCROLL_LEFT.value
94 | Action.SCROLL_UP.value
95 | Action.SCROLL_DOWN.value;
96 // Flags that make a node accessibilty focusable.
97 private static final int FOCUSABLE_FLAGS =
98 Flag.HAS_CHECKED_STATE.value
99 | Flag.IS_CHECKED.value
100 | Flag.IS_SELECTED.value
101 | Flag.IS_TEXT_FIELD.value
102 | Flag.IS_FOCUSED.value
103 | Flag.HAS_ENABLED_STATE.value
104 | Flag.IS_ENABLED.value
105 | Flag.IS_IN_MUTUALLY_EXCLUSIVE_GROUP.value
106 | Flag.HAS_TOGGLED_STATE.value
107 | Flag.IS_TOGGLED.value
108 | Flag.IS_FOCUSABLE.value
109 | Flag.IS_SLIDER.value;
110
111 // The minimal ID for an engine generated AccessibilityNodeInfo.
112 //
113 // The AccessibilityNodeInfo node IDs are generated by the framework for most Flutter semantic
114 // nodes.
115 // When embedding platform views, the framework does not have the accessibility information for
116 // the embedded view;
117 // in this case the engine generates AccessibilityNodeInfo that mirrors the a11y information
118 // exposed by the platform
119 // view. To avoid the need of synchronizing the framework and engine mechanisms for generating the
120 // next ID, we split
121 // the 32bit range of virtual node IDs into 2. The least significant 16 bits are used for
122 // framework generated IDs
123 // and the most significant 16 bits are used for engine generated IDs.
124 private static final int MIN_ENGINE_GENERATED_NODE_ID = 1 << 16;
125
126 // Font weight adjustment for bold text. FontWeight.Bold - FontWeight.Normal = w700 - w400 = 300.
127 private static final int BOLD_TEXT_WEIGHT_ADJUSTMENT = 300;
128
129 /// Value is derived from ACTION_TYPE_MASK in AccessibilityNodeInfo.java
130 private static int FIRST_RESOURCE_ID = 267386881;
131
132 // Real Android View, which internally holds a Flutter UI.
133 @NonNull private final View rootAccessibilityView;
134
135 // The accessibility communication API between Flutter's Android embedding and
136 // the Flutter framework.
137 @NonNull private final AccessibilityChannel accessibilityChannel;
138
139 // Android's {@link AccessibilityManager}, which we can query to see if accessibility is
140 // turned on, as well as listen for changes to accessibility's activation.
141 @NonNull private final AccessibilityManager accessibilityManager;
142
143 @NonNull private final AccessibilityViewEmbedder accessibilityViewEmbedder;
144
145 // The delegate for interacting with embedded platform views. Used to embed accessibility data for
146 // an embedded view in the accessibility tree.
147 @NonNull private final PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate;
148
149 // Android's {@link ContentResolver}, which is used to observe the global
150 // TRANSITION_ANIMATION_SCALE,
151 // which determines whether Flutter's animations should be enabled or disabled for accessibility
152 // purposes.
153 @NonNull private final ContentResolver contentResolver;
154
155 // The entire Flutter semantics tree of the running Flutter app, stored as a Map
156 // from each SemanticsNode's ID to a Java representation of a Flutter SemanticsNode.
157 //
158 // Flutter's semantics tree is cached here because Android might ask for information about
159 // a given SemanticsNode at any moment in time. Caching the tree allows for immediate
160 // response to Android's request.
161 //
162 // The structure of flutterSemanticsTree may be 1 or 2 frames behind the Flutter app
163 // due to the time required to communicate tree changes from Flutter to Android.
164 //
165 // See the Flutter docs on SemanticsNode:
166 // https://api.flutter.dev/flutter/semantics/SemanticsNode-class.html
167 @NonNull private final Map<Integer, SemanticsNode> flutterSemanticsTree = new HashMap<>();
168
169 // The set of all custom Flutter accessibility actions that are present in the running
170 // Flutter app, stored as a Map from each action's ID to the definition of the custom
171 // accessibility
172 // action.
173 //
174 // Flutter and Android support a number of built-in accessibility actions. However, these
175 // predefined actions are not always sufficient for a desired interaction. Android facilitates
176 // custom accessibility actions,
177 // https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction.
178 // Flutter supports custom accessibility actions via {@code customSemanticsActions} within
179 // a {@code Semantics} widget, https://api.flutter.dev/flutter/widgets/Semantics-class.html.
180 // {@code customAccessibilityActions} are an Android-side cache of all custom accessibility
181 // types declared within the running Flutter app.
182 //
183 // Custom accessibility actions are comprised of only a few fields, and therefore it is likely
184 // that a given app may define the same custom accessibility action many times. Identical
185 // custom accessibility actions are de-duped such that {@code customAccessibilityActions} only
186 // caches unique custom accessibility actions.
187 //
188 // See the Android documentation for custom accessibility actions:
189 // https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction
190 //
191 // See the Flutter documentation for the Semantics widget:
192 // https://api.flutter.dev/flutter/widgets/Semantics-class.html
193 @NonNull
194 private final Map<Integer, CustomAccessibilityAction> customAccessibilityActions =
195 new HashMap<>();
196
197 // The {@code SemanticsNode} within Flutter that currently has the focus of Android's
198 // accessibility system.
199 //
200 // This is null when a node embedded by the AccessibilityViewEmbedder has the focus.
201 @Nullable private SemanticsNode accessibilityFocusedSemanticsNode;
202
203 // The virtual ID of the currently embedded node with accessibility focus.
204 //
205 // This is the ID of a node generated by the AccessibilityViewEmbedder if an embedded node is
206 // focused,
207 // null otherwise.
208 private Integer embeddedAccessibilityFocusedNodeId;
209
210 // The virtual ID of the currently embedded node with input focus.
211 //
212 // This is the ID of a node generated by the AccessibilityViewEmbedder if an embedded node is
213 // focused,
214 // null otherwise.
215 private Integer embeddedInputFocusedNodeId;
216
217 // The accessibility features that should currently be active within Flutter, represented as
218 // a bitmask whose values comes from {@link AccessibilityFeature}.
219 private int accessibilityFeatureFlags = 0;
220
221 // The {@code SemanticsNode} within Flutter that currently has the focus of Android's input
222 // system.
223 //
224 // Input focus is independent of accessibility focus. It is possible that accessibility focus
225 // and input focus target the same {@code SemanticsNode}, but it is also possible that one
226 // {@code SemanticsNode} has input focus while a different {@code SemanticsNode} has
227 // accessibility focus. For example, a user may use a D-Pad to navigate to a text field, giving
228 // it accessibility focus, and then enable input on that text field, giving it input focus. Then
229 // the user moves the accessibility focus to a nearby label to get info about the label, while
230 // maintaining input focus on the original text field.
231 @Nullable private SemanticsNode inputFocusedSemanticsNode;
232
233 // Keeps track of the last semantics node that had the input focus.
234 //
235 // This is used to determine if the input focus has changed since the last time the
236 // {@code inputFocusSemanticsNode} has been set, so that we can send a {@code TYPE_VIEW_FOCUSED}
237 // event when it changes.
238 @Nullable private SemanticsNode lastInputFocusedSemanticsNode;
239
240 // The widget within Flutter that currently sits beneath a cursor, e.g,
241 // beneath a stylus or mouse cursor.
242 @Nullable private SemanticsNode hoveredObject;
243
244 @VisibleForTesting
245 public int getHoveredObjectId() {
246 return hoveredObject.id;
247 }
248
249 // A Java/Android cached representation of the Flutter app's navigation stack. The Flutter
250 // navigation stack is tracked so that accessibility announcements can be made during Flutter's
251 // navigation changes.
252 // TODO(mattcarroll): take this cache into account for new routing solution so accessibility does
253 // not get left behind.
254 @NonNull private final List<Integer> flutterNavigationStack = new ArrayList<>();
255
256 // TODO(mattcarroll): why do we need previouseRouteId if we have flutterNavigationStack
257 private int previousRouteId = ROOT_NODE_ID;
258
259 // Tracks the left system inset of the screen because Flutter needs to manually adjust
260 // accessibility positioning when in reverse-landscape. This is an Android bug that Flutter
261 // is solving for itself.
262 @NonNull private Integer lastLeftFrameInset = 0;
263
264 @Nullable private OnAccessibilityChangeListener onAccessibilityChangeListener;
265
266 // Whether the users are using assistive technologies to interact with the devices.
267 //
268 // The getter returns true when at least one of the assistive technologies is running:
269 // TalkBack, SwitchAccess, or VoiceAccess.
270 @VisibleForTesting
271 public boolean getAccessibleNavigation() {
272 return accessibleNavigation;
273 }
274
275 private boolean accessibleNavigation = false;
276
277 private void setAccessibleNavigation(boolean value) {
278 if (accessibleNavigation == value) {
279 return;
280 }
281 accessibleNavigation = value;
282 if (accessibleNavigation) {
283 accessibilityFeatureFlags |= AccessibilityFeature.ACCESSIBLE_NAVIGATION.value;
284 } else {
285 accessibilityFeatureFlags &= ~AccessibilityFeature.ACCESSIBLE_NAVIGATION.value;
286 }
287 sendLatestAccessibilityFlagsToFlutter();
288 }
289
290 // Set to true after {@code release} has been invoked.
291 private boolean isReleased = false;
292
293 // Handler for all messages received from Flutter via the {@code accessibilityChannel}
294 private final AccessibilityChannel.AccessibilityMessageHandler accessibilityMessageHandler =
295 new AccessibilityChannel.AccessibilityMessageHandler() {
296 /** The Dart application would like the given {@code message} to be announced. */
297 @Override
298 public void announce(@NonNull String message) {
299 rootAccessibilityView.announceForAccessibility(message);
300 }
301
302 /** The user has tapped on the widget with the given {@code nodeId}. */
303 @Override
304 public void onTap(int nodeId) {
305 sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_CLICKED);
306 }
307
308 /** The user has long pressed on the widget with the given {@code nodeId}. */
309 @Override
310 public void onLongPress(int nodeId) {
311 sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
312 }
313
314 /** The framework has requested focus on the given {@code nodeId}. */
315 @Override
316 public void onFocus(int nodeId) {
317 sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_FOCUSED);
318 }
319
320 /** The user has opened a tooltip. */
321 @Override
322 public void onTooltip(@NonNull String message) {
323 // Native Android tooltip is no longer announced when it pops up after API 28 and is
324 // handled by
325 // AccessibilityNodeInfo.setTooltipText instead.
326 //
327 // To reproduce native behavior, see
328 // https://developer.android.com/guide/topics/ui/tooltips.
329 if (Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
330 return;
331 }
332 AccessibilityEvent e =
333 obtainAccessibilityEvent(ROOT_NODE_ID, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
334 e.getText().add(message);
335 sendAccessibilityEvent(e);
336 }
337
338 /** New custom accessibility actions exist in Flutter. Update our Android-side cache. */
339 @Override
340 public void updateCustomAccessibilityActions(ByteBuffer buffer, String[] strings) {
341 buffer.order(ByteOrder.LITTLE_ENDIAN);
342 AccessibilityBridge.this.updateCustomAccessibilityActions(buffer, strings);
343 }
344
345 /** Flutter's semantics tree has changed. Update our Android-side cache. */
346 @Override
347 public void updateSemantics(
348 ByteBuffer buffer, String[] strings, ByteBuffer[] stringAttributeArgs) {
349 buffer.order(ByteOrder.LITTLE_ENDIAN);
350 for (ByteBuffer args : stringAttributeArgs) {
351 args.order(ByteOrder.LITTLE_ENDIAN);
352 }
353 AccessibilityBridge.this.updateSemantics(buffer, strings, stringAttributeArgs);
354 }
355 };
356
357 // Listener that is notified when accessibility is turned on/off.
358 private final AccessibilityManager.AccessibilityStateChangeListener
359 accessibilityStateChangeListener =
360 new AccessibilityManager.AccessibilityStateChangeListener() {
361 @Override
362 public void onAccessibilityStateChanged(boolean accessibilityEnabled) {
363 if (isReleased) {
364 return;
365 }
366 if (accessibilityEnabled) {
367 accessibilityChannel.setAccessibilityMessageHandler(accessibilityMessageHandler);
368 accessibilityChannel.onAndroidAccessibilityEnabled();
369 } else {
370 setAccessibleNavigation(false);
371 accessibilityChannel.setAccessibilityMessageHandler(null);
372 accessibilityChannel.onAndroidAccessibilityDisabled();
373 }
374
375 if (onAccessibilityChangeListener != null) {
376 onAccessibilityChangeListener.onAccessibilityChanged(
377 accessibilityEnabled, accessibilityManager.isTouchExplorationEnabled());
378 }
379 }
380 };
381
382 // Listener that is notified when accessibility touch exploration is turned on/off.
383 // This is guarded at instantiation time.
384 private final AccessibilityManager.TouchExplorationStateChangeListener
385 touchExplorationStateChangeListener;
386
387 // Listener that is notified when the global TRANSITION_ANIMATION_SCALE. When this scale goes
388 // to zero, we instruct Flutter to disable animations.
389 private final ContentObserver animationScaleObserver =
390 new ContentObserver(new Handler()) {
391 @Override
392 public void onChange(boolean selfChange) {
393 this.onChange(selfChange, null);
394 }
395
396 @Override
397 public void onChange(boolean selfChange, Uri uri) {
398 if (isReleased) {
399 return;
400 }
401 // Retrieve the current value of TRANSITION_ANIMATION_SCALE from the OS.
402 String value =
403 Settings.Global.getString(
404 contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE);
405
406 boolean shouldAnimationsBeDisabled = value != null && value.equals("0");
407 if (shouldAnimationsBeDisabled) {
408 accessibilityFeatureFlags |= AccessibilityFeature.DISABLE_ANIMATIONS.value;
409 } else {
410 accessibilityFeatureFlags &= ~AccessibilityFeature.DISABLE_ANIMATIONS.value;
411 }
412 sendLatestAccessibilityFlagsToFlutter();
413 }
414 };
415
417 @NonNull View rootAccessibilityView,
418 @NonNull AccessibilityChannel accessibilityChannel,
419 @NonNull AccessibilityManager accessibilityManager,
420 @NonNull ContentResolver contentResolver,
421 @NonNull PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) {
422 this(
423 rootAccessibilityView,
424 accessibilityChannel,
425 accessibilityManager,
426 contentResolver,
427 new AccessibilityViewEmbedder(rootAccessibilityView, MIN_ENGINE_GENERATED_NODE_ID),
428 platformViewsAccessibilityDelegate);
429 }
430
431 @VisibleForTesting
433 @NonNull View rootAccessibilityView,
434 @NonNull AccessibilityChannel accessibilityChannel,
435 @NonNull AccessibilityManager accessibilityManager,
436 @NonNull ContentResolver contentResolver,
437 @NonNull AccessibilityViewEmbedder accessibilityViewEmbedder,
438 @NonNull PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) {
439 this.rootAccessibilityView = rootAccessibilityView;
440 this.accessibilityChannel = accessibilityChannel;
441 this.accessibilityManager = accessibilityManager;
442 this.contentResolver = contentResolver;
443 this.accessibilityViewEmbedder = accessibilityViewEmbedder;
444 this.platformViewsAccessibilityDelegate = platformViewsAccessibilityDelegate;
445 // Tell Flutter whether accessibility is initially active or not. Then register a listener
446 // to be notified of changes in the future.
447 accessibilityStateChangeListener.onAccessibilityStateChanged(accessibilityManager.isEnabled());
448 this.accessibilityManager.addAccessibilityStateChangeListener(accessibilityStateChangeListener);
449
450 // Tell Flutter whether touch exploration is initially active or not. Then register a listener
451 // to be notified of changes in the future.
452 touchExplorationStateChangeListener =
453 new AccessibilityManager.TouchExplorationStateChangeListener() {
454 @Override
455 public void onTouchExplorationStateChanged(boolean isTouchExplorationEnabled) {
456 if (isReleased) {
457 return;
458 }
459 if (!isTouchExplorationEnabled) {
460 setAccessibleNavigation(false);
461 onTouchExplorationExit();
462 }
463
464 if (onAccessibilityChangeListener != null) {
465 onAccessibilityChangeListener.onAccessibilityChanged(
466 accessibilityManager.isEnabled(), isTouchExplorationEnabled);
467 }
468 }
469 };
470 touchExplorationStateChangeListener.onTouchExplorationStateChanged(
471 accessibilityManager.isTouchExplorationEnabled());
472 this.accessibilityManager.addTouchExplorationStateChangeListener(
473 touchExplorationStateChangeListener);
474
475 // Tell Flutter whether animations should initially be enabled or disabled. Then register a
476 // listener to be notified of changes in the future.
477 animationScaleObserver.onChange(false);
478 Uri transitionUri = Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE);
479 this.contentResolver.registerContentObserver(transitionUri, false, animationScaleObserver);
480
481 // Tells Flutter whether the text should be bolded or not. If the user changes bold text
482 // setting, the configuration will change and trigger a re-build of the accesibiltyBridge.
483 if (Build.VERSION.SDK_INT >= API_LEVELS.API_31) {
484 setBoldTextFlag();
485 }
486
487 platformViewsAccessibilityDelegate.attachAccessibilityBridge(this);
488 }
489
490 /**
491 * Disconnects any listeners and/or delegates that were initialized in {@code
492 * AccessibilityBridge}'s constructor, or added after.
493 *
494 * <p>Do not use this instance after invoking {@code release}. The behavior of any method invoked
495 * on this {@code AccessibilityBridge} after invoking {@code release()} is undefined.
496 */
497 public void release() {
498 isReleased = true;
499 platformViewsAccessibilityDelegate.detachAccessibilityBridge();
500 setOnAccessibilityChangeListener(null);
501 accessibilityManager.removeAccessibilityStateChangeListener(accessibilityStateChangeListener);
502 accessibilityManager.removeTouchExplorationStateChangeListener(
503 touchExplorationStateChangeListener);
504 contentResolver.unregisterContentObserver(animationScaleObserver);
505 accessibilityChannel.setAccessibilityMessageHandler(null);
506 }
507
508 /** Returns true if the Android OS currently has accessibility enabled, false otherwise. */
509 public boolean isAccessibilityEnabled() {
510 return accessibilityManager.isEnabled();
511 }
512
513 /** Returns true if the Android OS currently has touch exploration enabled, false otherwise. */
514 public boolean isTouchExplorationEnabled() {
515 return accessibilityManager.isTouchExplorationEnabled();
516 }
517
518 /**
519 * Sets a listener on this {@code AccessibilityBridge}, which is notified whenever accessibility
520 * activation, or touch exploration activation changes.
521 */
522 public void setOnAccessibilityChangeListener(@Nullable OnAccessibilityChangeListener listener) {
523 this.onAccessibilityChangeListener = listener;
524 }
525
526 /** Sends the current value of {@link #accessibilityFeatureFlags} to Flutter. */
527 private void sendLatestAccessibilityFlagsToFlutter() {
528 accessibilityChannel.setAccessibilityFeatures(accessibilityFeatureFlags);
529 }
530
531 private boolean shouldSetCollectionInfo(final SemanticsNode semanticsNode) {
532 // TalkBack expects a number of rows and/or columns greater than 0 to announce
533 // in list and out of list. For an infinite or growing list, you have to
534 // specify something > 0 to get "in list" announcements.
535 // TalkBack will also only track one list at a time, so we only want to set this
536 // for a list that contains the current a11y focused semanticsNode - otherwise, if there
537 // are two lists or nested lists, we may end up with announcements for only the last
538 // one that is currently available in the semantics tree. However, we also want
539 // to set it if we're exiting a list to a non-list, so that we can get the "out of list"
540 // announcement when A11y focus moves out of a list and not into another list.
541 return semanticsNode.scrollChildren > 0
542 && (SemanticsNode.nullableHasAncestor(
543 accessibilityFocusedSemanticsNode, o -> o == semanticsNode)
544 || !SemanticsNode.nullableHasAncestor(
545 accessibilityFocusedSemanticsNode, o -> o.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)));
546 }
547
548 @TargetApi(API_LEVELS.API_31)
549 @RequiresApi(API_LEVELS.API_31)
550 private void setBoldTextFlag() {
551 if (rootAccessibilityView == null || rootAccessibilityView.getResources() == null) {
552 return;
553 }
554 int fontWeightAdjustment =
555 rootAccessibilityView.getResources().getConfiguration().fontWeightAdjustment;
556 boolean shouldBold =
557 fontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED
558 && fontWeightAdjustment >= BOLD_TEXT_WEIGHT_ADJUSTMENT;
559
560 if (shouldBold) {
561 accessibilityFeatureFlags |= AccessibilityFeature.BOLD_TEXT.value;
562 } else {
563 accessibilityFeatureFlags &= AccessibilityFeature.BOLD_TEXT.value;
564 }
565 sendLatestAccessibilityFlagsToFlutter();
566 }
567
568 @VisibleForTesting
569 public AccessibilityNodeInfo obtainAccessibilityNodeInfo(View rootView) {
570 return AccessibilityNodeInfo.obtain(rootView);
571 }
572
573 @VisibleForTesting
574 public AccessibilityNodeInfo obtainAccessibilityNodeInfo(View rootView, int virtualViewId) {
575 return AccessibilityNodeInfo.obtain(rootView, virtualViewId);
576 }
577
578 /**
579 * Returns {@link AccessibilityNodeInfo} for the view corresponding to the given {@code
580 * virtualViewId}.
581 *
582 * <p>This method is invoked by Android's accessibility system when Android needs accessibility
583 * info for a given view.
584 *
585 * <p>When a {@code virtualViewId} of {@link View#NO_ID} is requested, accessibility node info is
586 * returned for our {@link #rootAccessibilityView}. Otherwise, Flutter's semantics tree,
587 * represented by {@link #flutterSemanticsTree}, is searched for a {@link SemanticsNode} with the
588 * given {@code virtualViewId}. If no such {@link SemanticsNode} is found, then this method
589 * returns null. If the desired {@link SemanticsNode} is found, then an {@link
590 * AccessibilityNodeInfo} is obtained from the {@link #rootAccessibilityView}, filled with
591 * appropriate info, and then returned.
592 *
593 * <p>Depending on the type of Flutter {@code SemanticsNode} that is requested, the returned
594 * {@link AccessibilityNodeInfo} pretends that the {@code SemanticsNode} in question comes from a
595 * specialize Android view, e.g., {@link Flag#IS_TEXT_FIELD} maps to {@code
596 * android.widget.EditText}, {@link Flag#IS_BUTTON} maps to {@code android.widget.Button}, and
597 * {@link Flag#IS_IMAGE} maps to {@code android.widget.ImageView}. In the case that no specialized
598 * view applies, the returned {@link AccessibilityNodeInfo} pretends that it represents a {@code
599 * android.view.View}.
600 */
601 @Override
602 @SuppressWarnings("deprecation")
603 // Suppressing Lint warning for new API, as we are version guarding all calls to newer APIs
604 @SuppressLint("NewApi")
605 public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
606 setAccessibleNavigation(true);
607 if (virtualViewId >= MIN_ENGINE_GENERATED_NODE_ID) {
608 // The node is in the engine generated range, and is provided by the accessibility view
609 // embedder.
610 return accessibilityViewEmbedder.createAccessibilityNodeInfo(virtualViewId);
611 }
612
613 if (virtualViewId == View.NO_ID) {
614 AccessibilityNodeInfo result = obtainAccessibilityNodeInfo(rootAccessibilityView);
615 rootAccessibilityView.onInitializeAccessibilityNodeInfo(result);
616 // TODO(mattcarroll): what does it mean for the semantics tree to contain or not contain
617 // the root node ID?
618 if (flutterSemanticsTree.containsKey(ROOT_NODE_ID)) {
619 result.addChild(rootAccessibilityView, ROOT_NODE_ID);
620 }
621 if (Build.VERSION.SDK_INT >= API_LEVELS.API_24) {
622 result.setImportantForAccessibility(false);
623 }
624 return result;
625 }
626
627 SemanticsNode semanticsNode = flutterSemanticsTree.get(virtualViewId);
628 if (semanticsNode == null) {
629 return null;
630 }
631
632 // Generate accessibility node for platform views using a virtual display.
633 //
634 // In this case, register the accessibility node in the view embedder,
635 // so the accessibility tree can be mirrored as a subtree of the Flutter accessibility tree.
636 // This is in constrast to hybrid composition where the embedded view is in the view hiearchy,
637 // so it doesn't need to be mirrored.
638 //
639 // See the case down below for how hybrid composition is handled.
640 if (semanticsNode.platformViewId != -1) {
641 if (platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
642 View embeddedView =
643 platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
644 if (embeddedView == null) {
645 return null;
646 }
647 Rect bounds = semanticsNode.getGlobalRect();
648 return accessibilityViewEmbedder.getRootNode(embeddedView, semanticsNode.id, bounds);
649 }
650 }
651
652 AccessibilityNodeInfo result =
653 obtainAccessibilityNodeInfo(rootAccessibilityView, virtualViewId);
654
655 // Accessibility Scanner uses isImportantForAccessibility to decide whether to check
656 // or skip this node.
657 if (Build.VERSION.SDK_INT >= API_LEVELS.API_24) {
658 result.setImportantForAccessibility(isImportant(semanticsNode));
659 }
660
661 // Work around for https://github.com/flutter/flutter/issues/21030
662 result.setViewIdResourceName("");
663 if (semanticsNode.identifier != null) {
664 result.setViewIdResourceName(semanticsNode.identifier);
665 }
666 result.setPackageName(rootAccessibilityView.getContext().getPackageName());
667 result.setClassName("android.view.View");
668 result.setSource(rootAccessibilityView, virtualViewId);
669 result.setFocusable(semanticsNode.isFocusable());
670 if (inputFocusedSemanticsNode != null) {
671 result.setFocused(inputFocusedSemanticsNode.id == virtualViewId);
672 }
673
674 if (accessibilityFocusedSemanticsNode != null) {
675 result.setAccessibilityFocused(accessibilityFocusedSemanticsNode.id == virtualViewId);
676 }
677
678 if (semanticsNode.hasFlag(Flag.IS_TEXT_FIELD)) {
679 result.setPassword(semanticsNode.hasFlag(Flag.IS_OBSCURED));
680 if (!semanticsNode.hasFlag(Flag.IS_READ_ONLY)) {
681 result.setClassName("android.widget.EditText");
682 }
683 result.setEditable(!semanticsNode.hasFlag(Flag.IS_READ_ONLY));
684 if (semanticsNode.textSelectionBase != -1 && semanticsNode.textSelectionExtent != -1) {
685 result.setTextSelection(semanticsNode.textSelectionBase, semanticsNode.textSelectionExtent);
686 }
687 // Text fields will always be created as a live region when they have input focus,
688 // so that updates to the label trigger polite announcements. This makes it easy to
689 // follow a11y guidelines for text fields on Android.
690 if (accessibilityFocusedSemanticsNode != null
691 && accessibilityFocusedSemanticsNode.id == virtualViewId) {
692 result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
693 }
694
695 // Cursor movements
696 int granularities = 0;
697 if (semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) {
698 result.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
699 granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER;
700 }
701 if (semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) {
702 result.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
703 granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER;
704 }
705 if (semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_WORD)) {
706 result.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
707 granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD;
708 }
709 if (semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_WORD)) {
710 result.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
711 granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD;
712 }
713 result.setMovementGranularities(granularities);
714 if (semanticsNode.maxValueLength >= 0) {
715 // Account for the fact that Flutter is counting Unicode scalar values and Android
716 // is counting UTF16 words.
717 final int length = semanticsNode.value == null ? 0 : semanticsNode.value.length();
718 int a = length - semanticsNode.currentValueLength + semanticsNode.maxValueLength;
719 result.setMaxTextLength(
720 length - semanticsNode.currentValueLength + semanticsNode.maxValueLength);
721 }
722 }
723
724 // These are non-ops on older devices. Attempting to interact with the text will cause Talkback
725 // to read the contents of the text box instead.
726 if (semanticsNode.hasAction(Action.SET_SELECTION)) {
727 result.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
728 }
729 if (semanticsNode.hasAction(Action.COPY)) {
730 result.addAction(AccessibilityNodeInfo.ACTION_COPY);
731 }
732 if (semanticsNode.hasAction(Action.CUT)) {
733 result.addAction(AccessibilityNodeInfo.ACTION_CUT);
734 }
735 if (semanticsNode.hasAction(Action.PASTE)) {
736 result.addAction(AccessibilityNodeInfo.ACTION_PASTE);
737 }
738
739 if (semanticsNode.hasAction(Action.SET_TEXT)) {
740 result.addAction(AccessibilityNodeInfo.ACTION_SET_TEXT);
741 }
742
743 if (semanticsNode.hasFlag(Flag.IS_BUTTON) || semanticsNode.hasFlag(Flag.IS_LINK)) {
744 result.setClassName("android.widget.Button");
745 }
746 if (semanticsNode.hasFlag(Flag.IS_IMAGE)) {
747 result.setClassName("android.widget.ImageView");
748 // TODO(jonahwilliams): Figure out a way conform to the expected id from TalkBack's
749 // CustomLabelManager. talkback/src/main/java/labeling/CustomLabelManager.java#L525
750 }
751 if (semanticsNode.hasAction(Action.DISMISS)) {
752 result.setDismissable(true);
753 result.addAction(AccessibilityNodeInfo.ACTION_DISMISS);
754 }
755
756 if (semanticsNode.parent != null) {
757 if (BuildConfig.DEBUG && semanticsNode.id <= ROOT_NODE_ID) {
758 Log.e(TAG, "Semantics node id is not > ROOT_NODE_ID.");
759 }
760 result.setParent(rootAccessibilityView, semanticsNode.parent.id);
761 } else {
762 if (BuildConfig.DEBUG && semanticsNode.id != ROOT_NODE_ID) {
763 Log.e(TAG, "Semantics node id does not equal ROOT_NODE_ID.");
764 }
765 result.setParent(rootAccessibilityView);
766 }
767
768 if (semanticsNode.previousNodeId != -1 && Build.VERSION.SDK_INT >= API_LEVELS.API_22) {
769 result.setTraversalAfter(rootAccessibilityView, semanticsNode.previousNodeId);
770 }
771
772 Rect bounds = semanticsNode.getGlobalRect();
773 if (semanticsNode.parent != null) {
774 Rect parentBounds = semanticsNode.parent.getGlobalRect();
775 Rect boundsInParent = new Rect(bounds);
776 boundsInParent.offset(-parentBounds.left, -parentBounds.top);
777 result.setBoundsInParent(boundsInParent);
778 } else {
779 result.setBoundsInParent(bounds);
780 }
781 final Rect boundsInScreen = getBoundsInScreen(bounds);
782 result.setBoundsInScreen(boundsInScreen);
783 result.setVisibleToUser(true);
784 result.setEnabled(
785 !semanticsNode.hasFlag(Flag.HAS_ENABLED_STATE) || semanticsNode.hasFlag(Flag.IS_ENABLED));
786
787 if (semanticsNode.hasAction(Action.TAP)) {
788 if (semanticsNode.onTapOverride != null) {
789 result.addAction(
790 new AccessibilityNodeInfo.AccessibilityAction(
791 AccessibilityNodeInfo.ACTION_CLICK, semanticsNode.onTapOverride.hint));
792 result.setClickable(true);
793 } else {
794 result.addAction(AccessibilityNodeInfo.ACTION_CLICK);
795 result.setClickable(true);
796 }
797 }
798 if (semanticsNode.hasAction(Action.LONG_PRESS)) {
799 if (semanticsNode.onLongPressOverride != null) {
800 result.addAction(
801 new AccessibilityNodeInfo.AccessibilityAction(
802 AccessibilityNodeInfo.ACTION_LONG_CLICK, semanticsNode.onLongPressOverride.hint));
803 result.setLongClickable(true);
804 } else {
805 result.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
806 result.setLongClickable(true);
807 }
808 }
809 if (semanticsNode.hasAction(Action.SCROLL_LEFT)
810 || semanticsNode.hasAction(Action.SCROLL_UP)
811 || semanticsNode.hasAction(Action.SCROLL_RIGHT)
812 || semanticsNode.hasAction(Action.SCROLL_DOWN)) {
813 result.setScrollable(true);
814
815 // This tells Android's a11y to send scroll events when reaching the end of
816 // the visible viewport of a scrollable, unless the node itself does not
817 // allow implicit scrolling - then we leave the className as view.View.
818 //
819 // We should prefer setCollectionInfo to the class names, as this way we get "In List"
820 // and "Out of list" announcements. But we don't always know the counts, so we
821 // can fallback to the generic scroll view class names.
822 //
823 // On older APIs, we always fall back to the generic scroll view class names here.
824 //
825 // TODO(dnfield): We should add semantics properties for rows and columns in 2 dimensional
826 // lists, e.g.
827 // GridView. Right now, we're only supporting ListViews and only if they have scroll
828 // children.
829 if (semanticsNode.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)) {
830 if (semanticsNode.hasAction(Action.SCROLL_LEFT)
831 || semanticsNode.hasAction(Action.SCROLL_RIGHT)) {
832 if (shouldSetCollectionInfo(semanticsNode)) {
833 result.setCollectionInfo(
834 AccessibilityNodeInfo.CollectionInfo.obtain(
835 0, // rows
836 semanticsNode.scrollChildren, // columns
837 false // hierarchical
838 ));
839 } else {
840 result.setClassName("android.widget.HorizontalScrollView");
841 }
842 } else {
843 if (shouldSetCollectionInfo(semanticsNode)) {
844 result.setCollectionInfo(
845 AccessibilityNodeInfo.CollectionInfo.obtain(
846 semanticsNode.scrollChildren, // rows
847 0, // columns
848 false // hierarchical
849 ));
850 } else {
851 result.setClassName("android.widget.ScrollView");
852 }
853 }
854 }
855 // TODO(ianh): Once we're on SDK v23+, call addAction to
856 // expose AccessibilityAction.ACTION_SCROLL_LEFT, _RIGHT,
857 // _UP, and _DOWN when appropriate.
858 if (semanticsNode.hasAction(Action.SCROLL_LEFT)
859 || semanticsNode.hasAction(Action.SCROLL_UP)) {
860 result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
861 }
862 if (semanticsNode.hasAction(Action.SCROLL_RIGHT)
863 || semanticsNode.hasAction(Action.SCROLL_DOWN)) {
864 result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
865 }
866 }
867 if (semanticsNode.hasAction(Action.INCREASE) || semanticsNode.hasAction(Action.DECREASE)) {
868 // TODO(jonahwilliams): support AccessibilityAction.ACTION_SET_PROGRESS once SDK is
869 // updated.
870 result.setClassName("android.widget.SeekBar");
871 if (semanticsNode.hasAction(Action.INCREASE)) {
872 result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
873 }
874 if (semanticsNode.hasAction(Action.DECREASE)) {
875 result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
876 }
877 }
878 if (semanticsNode.hasFlag(Flag.IS_LIVE_REGION)) {
879 result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
880 }
881
882 // Scopes routes are not focusable, only need to set the content
883 // for non-scopes-routes semantics nodes.
884 if (semanticsNode.hasFlag(Flag.IS_TEXT_FIELD)) {
885 result.setText(semanticsNode.getValue());
886 if (Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
887 result.setHintText(semanticsNode.getTextFieldHint());
888 }
889 } else if (!semanticsNode.hasFlag(Flag.SCOPES_ROUTE)) {
890 CharSequence content = semanticsNode.getValueLabelHint();
891 if (Build.VERSION.SDK_INT < API_LEVELS.API_28) {
892 if (semanticsNode.tooltip != null) {
893 // For backward compatibility with Flutter SDK before Android API
894 // level 28, the tooltip is appended at the end of content description.
895 content = content != null ? content : "";
896 content = content + "\n" + semanticsNode.tooltip;
897 }
898 }
899 if (content != null) {
900 result.setContentDescription(content);
901 }
902 }
903
904 if (Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
905 if (semanticsNode.tooltip != null) {
906 result.setTooltipText(semanticsNode.tooltip);
907 }
908 }
909
910 boolean hasCheckedState = semanticsNode.hasFlag(Flag.HAS_CHECKED_STATE);
911 boolean hasToggledState = semanticsNode.hasFlag(Flag.HAS_TOGGLED_STATE);
912 if (BuildConfig.DEBUG && (hasCheckedState && hasToggledState)) {
913 Log.e(TAG, "Expected semanticsNode to have checked state and toggled state.");
914 }
915 result.setCheckable(hasCheckedState || hasToggledState);
916 if (hasCheckedState) {
917 result.setChecked(semanticsNode.hasFlag(Flag.IS_CHECKED));
918 if (semanticsNode.hasFlag(Flag.IS_IN_MUTUALLY_EXCLUSIVE_GROUP)) {
919 result.setClassName("android.widget.RadioButton");
920 } else {
921 result.setClassName("android.widget.CheckBox");
922 }
923 } else if (hasToggledState) {
924 result.setChecked(semanticsNode.hasFlag(Flag.IS_TOGGLED));
925 result.setClassName("android.widget.Switch");
926 }
927 result.setSelected(semanticsNode.hasFlag(Flag.IS_SELECTED));
928
929 // Heading support
930 if (Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
931 result.setHeading(semanticsNode.hasFlag(Flag.IS_HEADER));
932 }
933
934 // Accessibility Focus
935 if (accessibilityFocusedSemanticsNode != null
936 && accessibilityFocusedSemanticsNode.id == virtualViewId) {
937 result.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
938 } else {
939 result.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
940 }
941
942 // Actions on the local context menu
943 if (semanticsNode.customAccessibilityActions != null) {
944 for (CustomAccessibilityAction action : semanticsNode.customAccessibilityActions) {
945 result.addAction(
946 new AccessibilityNodeInfo.AccessibilityAction(action.resourceId, action.label));
947 }
948 }
949
950 for (SemanticsNode child : semanticsNode.childrenInTraversalOrder) {
951 if (child.hasFlag(Flag.IS_HIDDEN)) {
952 continue;
953 }
954 if (child.platformViewId != -1) {
955 View embeddedView =
956 platformViewsAccessibilityDelegate.getPlatformViewById(child.platformViewId);
957
958 // Add the embedded view as a child of the current accessibility node if it's not
959 // using a virtual display.
960 //
961 // In this case, the view is in the Activity's view hierarchy, so it doesn't need to be
962 // mirrored.
963 //
964 // See the case above for how virtual displays are handled.
965 if (!platformViewsAccessibilityDelegate.usesVirtualDisplay(child.platformViewId)) {
966 result.addChild(embeddedView);
967 continue;
968 }
969 }
970 result.addChild(rootAccessibilityView, child.id);
971 }
972 return result;
973 }
974
975 private boolean isImportant(SemanticsNode node) {
976 if (node.hasFlag(Flag.SCOPES_ROUTE)) {
977 return false;
978 }
979
980 if (node.getValueLabelHint() != null) {
981 return true;
982 }
983
984 // Return true if the node has had any user action (not including system actions)
985 return (node.actions & ~systemAction) != 0;
986 }
987
988 /**
989 * Get the bounds in screen with root FlutterView's offset.
990 *
991 * @param bounds the bounds in FlutterView
992 * @return the bounds with offset
993 */
994 private Rect getBoundsInScreen(Rect bounds) {
995 Rect boundsInScreen = new Rect(bounds);
996 int[] locationOnScreen = new int[2];
997 rootAccessibilityView.getLocationOnScreen(locationOnScreen);
998 boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
999 return boundsInScreen;
1000 }
1001
1002 /**
1003 * Instructs the view represented by {@code virtualViewId} to carry out the desired {@code
1004 * accessibilityAction}, perhaps configured by additional {@code arguments}.
1005 *
1006 * <p>This method is invoked by Android's accessibility system. This method returns true if the
1007 * desired {@code SemanticsNode} was found and was capable of performing the desired action, false
1008 * otherwise.
1009 *
1010 * <p>In a traditional Android app, the given view ID refers to a {@link View} within an Android
1011 * {@link View} hierarchy. Flutter does not have an Android {@link View} hierarchy, therefore the
1012 * given view ID is a {@code virtualViewId} that refers to a {@code SemanticsNode} within a
1013 * Flutter app. The given arguments of this method are forwarded from Android to Flutter.
1014 */
1015 @Override
1016 public boolean performAction(
1017 int virtualViewId, int accessibilityAction, @Nullable Bundle arguments) {
1018 if (virtualViewId >= MIN_ENGINE_GENERATED_NODE_ID) {
1019 // The node is in the engine generated range, and is handled by the accessibility view
1020 // embedder.
1021 boolean didPerform =
1022 accessibilityViewEmbedder.performAction(virtualViewId, accessibilityAction, arguments);
1023 if (didPerform
1024 && accessibilityAction == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
1025 embeddedAccessibilityFocusedNodeId = null;
1026 }
1027 return didPerform;
1028 }
1029 SemanticsNode semanticsNode = flutterSemanticsTree.get(virtualViewId);
1030 if (semanticsNode == null) {
1031 return false;
1032 }
1033 switch (accessibilityAction) {
1034 case AccessibilityNodeInfo.ACTION_CLICK:
1035 {
1036 // Note: TalkBack prior to Oreo doesn't use this handler and instead simulates a
1037 // click event at the center of the SemanticsNode. Other a11y services might go
1038 // through this handler though.
1039 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.TAP);
1040 return true;
1041 }
1042 case AccessibilityNodeInfo.ACTION_LONG_CLICK:
1043 {
1044 // Note: TalkBack doesn't use this handler and instead simulates a long click event
1045 // at the center of the SemanticsNode. Other a11y services might go through this
1046 // handler though.
1047 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.LONG_PRESS);
1048 return true;
1049 }
1050 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
1051 {
1052 if (semanticsNode.hasAction(Action.SCROLL_UP)) {
1053 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.SCROLL_UP);
1054 } else if (semanticsNode.hasAction(Action.SCROLL_LEFT)) {
1055 // TODO(ianh): bidi support using textDirection
1056 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.SCROLL_LEFT);
1057 } else if (semanticsNode.hasAction(Action.INCREASE)) {
1058 semanticsNode.value = semanticsNode.increasedValue;
1059 semanticsNode.valueAttributes = semanticsNode.increasedValueAttributes;
1060 // Event causes Android to read out the updated value.
1061 sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
1062 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.INCREASE);
1063 } else {
1064 return false;
1065 }
1066 return true;
1067 }
1068 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
1069 {
1070 if (semanticsNode.hasAction(Action.SCROLL_DOWN)) {
1071 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.SCROLL_DOWN);
1072 } else if (semanticsNode.hasAction(Action.SCROLL_RIGHT)) {
1073 // TODO(ianh): bidi support using textDirection
1074 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.SCROLL_RIGHT);
1075 } else if (semanticsNode.hasAction(Action.DECREASE)) {
1076 semanticsNode.value = semanticsNode.decreasedValue;
1077 semanticsNode.valueAttributes = semanticsNode.decreasedValueAttributes;
1078 // Event causes Android to read out the updated value.
1079 sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
1080 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.DECREASE);
1081 } else {
1082 return false;
1083 }
1084 return true;
1085 }
1086 case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY:
1087 {
1088 return performCursorMoveAction(semanticsNode, virtualViewId, arguments, false);
1089 }
1090 case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY:
1091 {
1092 return performCursorMoveAction(semanticsNode, virtualViewId, arguments, true);
1093 }
1094 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
1095 {
1096 // Focused semantics node must be reset before sending the
1097 // TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED event. Otherwise,
1098 // TalkBack may think the node is still focused.
1099 if (accessibilityFocusedSemanticsNode != null
1100 && accessibilityFocusedSemanticsNode.id == virtualViewId) {
1101 accessibilityFocusedSemanticsNode = null;
1102 }
1103 if (embeddedAccessibilityFocusedNodeId != null
1104 && embeddedAccessibilityFocusedNodeId == virtualViewId) {
1105 embeddedAccessibilityFocusedNodeId = null;
1106 }
1107 accessibilityChannel.dispatchSemanticsAction(
1108 virtualViewId, Action.DID_LOSE_ACCESSIBILITY_FOCUS);
1109 sendAccessibilityEvent(
1110 virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
1111 return true;
1112 }
1113 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
1114 {
1115 if (accessibilityFocusedSemanticsNode == null) {
1116 // When Android focuses a node, it doesn't invalidate the view.
1117 // (It does when it sends ACTION_CLEAR_ACCESSIBILITY_FOCUS, so
1118 // we only have to worry about this when the focused node is null.)
1119 rootAccessibilityView.invalidate();
1120 }
1121 // Focused semantics node must be set before sending the TYPE_VIEW_ACCESSIBILITY_FOCUSED
1122 // event. Otherwise, TalkBack may think the node is not focused yet.
1123 accessibilityFocusedSemanticsNode = semanticsNode;
1124
1125 accessibilityChannel.dispatchSemanticsAction(
1126 virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS);
1127
1128 HashMap<String, Object> message = new HashMap<>();
1129 message.put("type", "didGainFocus");
1130 message.put("nodeId", semanticsNode.id);
1131 accessibilityChannel.channel.send(message);
1132
1133 sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
1134
1135 if (semanticsNode.hasAction(Action.INCREASE)
1136 || semanticsNode.hasAction(Action.DECREASE)) {
1137 // SeekBars only announce themselves after this event.
1138 sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
1139 }
1140
1141 return true;
1142 }
1143 case ACTION_SHOW_ON_SCREEN:
1144 {
1145 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.SHOW_ON_SCREEN);
1146 return true;
1147 }
1148 case AccessibilityNodeInfo.ACTION_SET_SELECTION:
1149 {
1150 final Map<String, Integer> selection = new HashMap<>();
1151 final boolean hasSelection =
1152 arguments != null
1153 && arguments.containsKey(
1154 AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT)
1155 && arguments.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT);
1156 if (hasSelection) {
1157 selection.put(
1158 "base",
1159 arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT));
1160 selection.put(
1161 "extent",
1162 arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT));
1163 } else {
1164 // Clear the selection
1165 selection.put("base", semanticsNode.textSelectionExtent);
1166 selection.put("extent", semanticsNode.textSelectionExtent);
1167 }
1168 accessibilityChannel.dispatchSemanticsAction(
1169 virtualViewId, Action.SET_SELECTION, selection);
1170 // The voice access expects the semantics node to update immediately. We update the
1171 // semantics node based on prediction. If the result is incorrect, it will be updated in
1172 // the next frame.
1173 SemanticsNode node = flutterSemanticsTree.get(virtualViewId);
1174 node.textSelectionBase = selection.get("base");
1175 node.textSelectionExtent = selection.get("extent");
1176 return true;
1177 }
1178 case AccessibilityNodeInfo.ACTION_COPY:
1179 {
1180 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.COPY);
1181 return true;
1182 }
1183 case AccessibilityNodeInfo.ACTION_CUT:
1184 {
1185 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.CUT);
1186 return true;
1187 }
1188 case AccessibilityNodeInfo.ACTION_PASTE:
1189 {
1190 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.PASTE);
1191 return true;
1192 }
1193 case AccessibilityNodeInfo.ACTION_DISMISS:
1194 {
1195 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.DISMISS);
1196 return true;
1197 }
1198 case AccessibilityNodeInfo.ACTION_SET_TEXT:
1199 {
1200 return performSetText(semanticsNode, virtualViewId, arguments);
1201 }
1202 default:
1203 // might be a custom accessibility accessibilityAction.
1204 final int flutterId = accessibilityAction - FIRST_RESOURCE_ID;
1205 CustomAccessibilityAction contextAction = customAccessibilityActions.get(flutterId);
1206 if (contextAction != null) {
1207 accessibilityChannel.dispatchSemanticsAction(
1208 virtualViewId, Action.CUSTOM_ACTION, contextAction.id);
1209 return true;
1210 }
1211 }
1212 return false;
1213 }
1214
1215 /**
1216 * Handles the responsibilities of {@link #performAction(int, int, Bundle)} for the specific
1217 * scenario of cursor movement.
1218 */
1219 private boolean performCursorMoveAction(
1220 @NonNull SemanticsNode semanticsNode,
1221 int virtualViewId,
1222 @NonNull Bundle arguments,
1223 boolean forward) {
1224 final int granularity =
1225 arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
1226 final boolean extendSelection =
1227 arguments.getBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
1228 // The voice access expects the semantics node to update immediately. We update the semantics
1229 // node based on prediction. If the result is incorrect, it will be updated in the next frame.
1230 final int previousTextSelectionBase = semanticsNode.textSelectionBase;
1231 final int previousTextSelectionExtent = semanticsNode.textSelectionExtent;
1232 predictCursorMovement(semanticsNode, granularity, forward, extendSelection);
1233
1234 if (previousTextSelectionBase != semanticsNode.textSelectionBase
1235 || previousTextSelectionExtent != semanticsNode.textSelectionExtent) {
1236 final String value = semanticsNode.value != null ? semanticsNode.value : "";
1237 final AccessibilityEvent selectionEvent =
1238 obtainAccessibilityEvent(
1239 semanticsNode.id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
1240 selectionEvent.getText().add(value);
1241 selectionEvent.setFromIndex(semanticsNode.textSelectionBase);
1242 selectionEvent.setToIndex(semanticsNode.textSelectionExtent);
1243 selectionEvent.setItemCount(value.length());
1244 sendAccessibilityEvent(selectionEvent);
1245 }
1246
1247 switch (granularity) {
1248 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER:
1249 {
1250 if (forward && semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) {
1251 accessibilityChannel.dispatchSemanticsAction(
1252 virtualViewId, Action.MOVE_CURSOR_FORWARD_BY_CHARACTER, extendSelection);
1253 return true;
1254 }
1255 if (!forward && semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) {
1256 accessibilityChannel.dispatchSemanticsAction(
1257 virtualViewId, Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER, extendSelection);
1258 return true;
1259 }
1260 break;
1261 }
1262 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD:
1263 if (forward && semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_WORD)) {
1264 accessibilityChannel.dispatchSemanticsAction(
1265 virtualViewId, Action.MOVE_CURSOR_FORWARD_BY_WORD, extendSelection);
1266 return true;
1267 }
1268 if (!forward && semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_WORD)) {
1269 accessibilityChannel.dispatchSemanticsAction(
1270 virtualViewId, Action.MOVE_CURSOR_BACKWARD_BY_WORD, extendSelection);
1271 return true;
1272 }
1273 break;
1274 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE:
1275 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH:
1276 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE:
1277 return true;
1278 }
1279 return false;
1280 }
1281
1282 private void predictCursorMovement(
1283 @NonNull SemanticsNode node, int granularity, boolean forward, boolean extendSelection) {
1284 if (node.textSelectionExtent < 0 || node.textSelectionBase < 0) {
1285 return;
1286 }
1287
1288 switch (granularity) {
1289 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER:
1290 if (forward && node.textSelectionExtent < node.value.length()) {
1291 node.textSelectionExtent += 1;
1292 } else if (!forward && node.textSelectionExtent > 0) {
1293 node.textSelectionExtent -= 1;
1294 }
1295 break;
1296 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD:
1297 if (forward && node.textSelectionExtent < node.value.length()) {
1298 Pattern pattern = Pattern.compile("\\p{L}(\\b)");
1299 Matcher result = pattern.matcher(node.value.substring(node.textSelectionExtent));
1300 // we discard the first result because we want to find the "next" word
1301 result.find();
1302 if (result.find()) {
1303 node.textSelectionExtent += result.start(1);
1304 } else {
1305 node.textSelectionExtent = node.value.length();
1306 }
1307 } else if (!forward && node.textSelectionExtent > 0) {
1308 // Finds last beginning of the word boundary.
1309 Pattern pattern = Pattern.compile("(?s:.*)(\\b)\\p{L}");
1310 Matcher result = pattern.matcher(node.value.substring(0, node.textSelectionExtent));
1311 if (result.find()) {
1312 node.textSelectionExtent = result.start(1);
1313 }
1314 }
1315 break;
1316 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE:
1317 if (forward && node.textSelectionExtent < node.value.length()) {
1318 // Finds the next new line.
1319 Pattern pattern = Pattern.compile("(?!^)(\\n)");
1320 Matcher result = pattern.matcher(node.value.substring(node.textSelectionExtent));
1321 if (result.find()) {
1322 node.textSelectionExtent += result.start(1);
1323 } else {
1324 node.textSelectionExtent = node.value.length();
1325 }
1326 } else if (!forward && node.textSelectionExtent > 0) {
1327 // Finds the last new line.
1328 Pattern pattern = Pattern.compile("(?s:.*)(\\n)");
1329 Matcher result = pattern.matcher(node.value.substring(0, node.textSelectionExtent));
1330 if (result.find()) {
1331 node.textSelectionExtent = result.start(1);
1332 } else {
1333 node.textSelectionExtent = 0;
1334 }
1335 }
1336 break;
1337 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH:
1338 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE:
1339 if (forward) {
1340 node.textSelectionExtent = node.value.length();
1341 } else {
1342 node.textSelectionExtent = 0;
1343 }
1344 break;
1345 }
1346 if (!extendSelection) {
1347 node.textSelectionBase = node.textSelectionExtent;
1348 }
1349 }
1350
1351 /**
1352 * Handles the responsibilities of {@link #performAction(int, int, Bundle)} for the specific
1353 * scenario of cursor movement.
1354 */
1355 private boolean performSetText(SemanticsNode node, int virtualViewId, @NonNull Bundle arguments) {
1356 String newText = "";
1357 if (arguments != null
1358 && arguments.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE)) {
1359 newText = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
1360 }
1361 accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.SET_TEXT, newText);
1362 // The voice access expects the semantics node to update immediately. Update the semantics
1363 // node based on prediction. If the result is incorrect, it will be updated in the next frame.
1364 node.value = newText;
1365 node.valueAttributes = null;
1366 return true;
1367 }
1368
1369 // TODO(ianh): implement findAccessibilityNodeInfosByText()
1370
1371 /**
1372 * Finds the view in a hierarchy that currently has the given type of {@code focus}.
1373 *
1374 * <p>This method is invoked by Android's accessibility system.
1375 *
1376 * <p>Flutter does not have an Android {@link View} hierarchy. Therefore, Flutter conceptually
1377 * handles this request by searching its semantics tree for the given {@code focus}, represented
1378 * by {@link #flutterSemanticsTree}. In practice, this {@code AccessibilityBridge} always caches
1379 * any active {@link #accessibilityFocusedSemanticsNode} and {@link #inputFocusedSemanticsNode}.
1380 * Therefore, no searching is necessary. This method directly inspects the given {@code focus}
1381 * type to return one of the cached nodes, null if the cached node is null, or null if a different
1382 * {@code focus} type is requested.
1383 */
1384 @Override
1385 public AccessibilityNodeInfo findFocus(int focus) {
1386 switch (focus) {
1387 case AccessibilityNodeInfo.FOCUS_INPUT:
1388 {
1389 if (inputFocusedSemanticsNode != null) {
1390 return createAccessibilityNodeInfo(inputFocusedSemanticsNode.id);
1391 }
1392 if (embeddedInputFocusedNodeId != null) {
1393 return createAccessibilityNodeInfo(embeddedInputFocusedNodeId);
1394 }
1395 }
1396 // Fall through to check FOCUS_ACCESSIBILITY
1397 case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY:
1398 {
1399 if (accessibilityFocusedSemanticsNode != null) {
1400 return createAccessibilityNodeInfo(accessibilityFocusedSemanticsNode.id);
1401 }
1402 if (embeddedAccessibilityFocusedNodeId != null) {
1403 return createAccessibilityNodeInfo(embeddedAccessibilityFocusedNodeId);
1404 }
1405 }
1406 }
1407 return null;
1408 }
1409
1410 /** Returns the {@link SemanticsNode} at the root of Flutter's semantics tree. */
1411 private SemanticsNode getRootSemanticsNode() {
1412 if (BuildConfig.DEBUG && !flutterSemanticsTree.containsKey(0)) {
1413 Log.e(TAG, "Attempted to getRootSemanticsNode without a root semantics node.");
1414 }
1415 return flutterSemanticsTree.get(0);
1416 }
1417
1418 /**
1419 * Returns an existing {@link SemanticsNode} with the given {@code id}, if it exists within {@link
1420 * #flutterSemanticsTree}, or creates and returns a new {@link SemanticsNode} with the given
1421 * {@code id}, adding the new {@link SemanticsNode} to the {@link #flutterSemanticsTree}.
1422 *
1423 * <p>This method should only be invoked as a result of receiving new information from Flutter.
1424 * The {@link #flutterSemanticsTree} is an Android cache of the last known state of a Flutter
1425 * app's semantics tree, therefore, invoking this method in any other situation will result in a
1426 * corrupt cache of Flutter's semantics tree.
1427 */
1428 private SemanticsNode getOrCreateSemanticsNode(int id) {
1429 SemanticsNode semanticsNode = flutterSemanticsTree.get(id);
1430 if (semanticsNode == null) {
1431 semanticsNode = new SemanticsNode(this);
1432 semanticsNode.id = id;
1433 flutterSemanticsTree.put(id, semanticsNode);
1434 }
1435 return semanticsNode;
1436 }
1437
1438 /**
1439 * Returns an existing {@link CustomAccessibilityAction} with the given {@code id}, if it exists
1440 * within {@link #customAccessibilityActions}, or creates and returns a new {@link
1441 * CustomAccessibilityAction} with the given {@code id}, adding the new {@link
1442 * CustomAccessibilityAction} to the {@link #customAccessibilityActions}.
1443 *
1444 * <p>This method should only be invoked as a result of receiving new information from Flutter.
1445 * The {@link #customAccessibilityActions} is an Android cache of the last known state of a
1446 * Flutter app's registered custom accessibility actions, therefore, invoking this method in any
1447 * other situation will result in a corrupt cache of Flutter's accessibility actions.
1448 */
1449 private CustomAccessibilityAction getOrCreateAccessibilityAction(int id) {
1450 CustomAccessibilityAction action = customAccessibilityActions.get(id);
1451 if (action == null) {
1452 action = new CustomAccessibilityAction();
1453 action.id = id;
1454 action.resourceId = id + FIRST_RESOURCE_ID;
1455 customAccessibilityActions.put(id, action);
1456 }
1457 return action;
1458 }
1459
1460 /**
1461 * A hover {@link MotionEvent} has occurred in the {@code View} that corresponds to this {@code
1462 * AccessibilityBridge}.
1463 *
1464 * <p>This method returns true if Flutter's accessibility system handled the hover event, false
1465 * otherwise.
1466 *
1467 * <p>This method should be invoked from the corresponding {@code View}'s {@link
1468 * View#onHoverEvent(MotionEvent)}.
1469 */
1470 public boolean onAccessibilityHoverEvent(MotionEvent event) {
1471 return onAccessibilityHoverEvent(event, false);
1472 }
1473
1474 /**
1475 * A hover {@link MotionEvent} has occurred in the {@code View} that corresponds to this {@code
1476 * AccessibilityBridge}.
1477 *
1478 * <p>If {@code ignorePlatformViews} is true, if hit testing for the event finds a platform view,
1479 * the event will not be handled. This is useful when handling accessibility events for views
1480 * overlaying platform views. See {@code PlatformOverlayView} for details.
1481 *
1482 * <p>This method returns true if Flutter's accessibility system handled the hover event, false
1483 * otherwise.
1484 *
1485 * <p>This method should be invoked from the corresponding {@code View}'s {@link
1486 * View#onHoverEvent(MotionEvent)}.
1487 */
1488 public boolean onAccessibilityHoverEvent(MotionEvent event, boolean ignorePlatformViews) {
1489 if (!accessibilityManager.isTouchExplorationEnabled()) {
1490 return false;
1491 }
1492 if (flutterSemanticsTree.isEmpty()) {
1493 return false;
1494 }
1495
1496 SemanticsNode semanticsNodeUnderCursor =
1497 getRootSemanticsNode()
1498 .hitTest(new float[] {event.getX(), event.getY(), 0, 1}, ignorePlatformViews);
1499 // semanticsNodeUnderCursor can be null when hovering over non-flutter UI such as
1500 // the Android navigation bar due to hitTest() bounds checking.
1501 if (semanticsNodeUnderCursor != null && semanticsNodeUnderCursor.platformViewId != -1) {
1502 if (ignorePlatformViews) {
1503 return false;
1504 }
1505 return accessibilityViewEmbedder.onAccessibilityHoverEvent(
1506 semanticsNodeUnderCursor.id, event);
1507 }
1508
1509 if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER
1510 || event.getAction() == MotionEvent.ACTION_HOVER_MOVE) {
1511 handleTouchExploration(event.getX(), event.getY(), ignorePlatformViews);
1512 } else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
1513 onTouchExplorationExit();
1514 } else {
1515 Log.d("flutter", "unexpected accessibility hover event: " + event);
1516 return false;
1517 }
1518 return true;
1519 }
1520
1521 /**
1522 * This method should be invoked when a hover interaction has the cursor move off of a {@code
1523 * SemanticsNode}.
1524 *
1525 * <p>This method informs the Android accessibility system that a {@link
1526 * AccessibilityEvent#TYPE_VIEW_HOVER_EXIT} has occurred.
1527 */
1528 private void onTouchExplorationExit() {
1529 if (hoveredObject != null) {
1530 sendAccessibilityEvent(hoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
1531 hoveredObject = null;
1532 }
1533 }
1534
1535 /**
1536 * This method should be invoked when a new hover interaction begins with a {@code SemanticsNode},
1537 * or when an existing hover interaction sees a movement of the cursor.
1538 *
1539 * <p>This method checks to see if the cursor has moved from one {@code SemanticsNode} to another.
1540 * If it has, this method informs the Android accessibility system of the change by first sending
1541 * a {@link AccessibilityEvent#TYPE_VIEW_HOVER_ENTER} event for the new hover node, followed by a
1542 * {@link AccessibilityEvent#TYPE_VIEW_HOVER_EXIT} event for the old hover node.
1543 */
1544 private void handleTouchExploration(float x, float y, boolean ignorePlatformViews) {
1545 if (flutterSemanticsTree.isEmpty()) {
1546 return;
1547 }
1548 SemanticsNode semanticsNodeUnderCursor =
1549 getRootSemanticsNode().hitTest(new float[] {x, y, 0, 1}, ignorePlatformViews);
1550 if (semanticsNodeUnderCursor != hoveredObject) {
1551 // sending ENTER before EXIT is how Android wants it
1552 if (semanticsNodeUnderCursor != null) {
1553 sendAccessibilityEvent(
1554 semanticsNodeUnderCursor.id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
1555 }
1556 if (hoveredObject != null) {
1557 sendAccessibilityEvent(hoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
1558 }
1559 hoveredObject = semanticsNodeUnderCursor;
1560 }
1561 }
1562
1563 /**
1564 * Updates the Android cache of Flutter's currently registered custom accessibility actions.
1565 *
1566 * <p>The buffer received here is encoded by PlatformViewAndroid::UpdateSemantics, and the decode
1567 * logic here must be kept in sync with that method's encoding logic.
1568 */
1569 // TODO(mattcarroll): Consider introducing ability to delete custom actions because they can
1570 // probably come and go in Flutter, so we may want to reflect that here in
1571 // the Android cache as well.
1572 void updateCustomAccessibilityActions(@NonNull ByteBuffer buffer, @NonNull String[] strings) {
1573 while (buffer.hasRemaining()) {
1574 int id = buffer.getInt();
1575 CustomAccessibilityAction action = getOrCreateAccessibilityAction(id);
1576 action.overrideId = buffer.getInt();
1577 int stringIndex = buffer.getInt();
1578 action.label = stringIndex == -1 ? null : strings[stringIndex];
1579 stringIndex = buffer.getInt();
1580 action.hint = stringIndex == -1 ? null : strings[stringIndex];
1581 }
1582 }
1583
1584 /**
1585 * Updates {@link #flutterSemanticsTree} to reflect the latest state of Flutter's semantics tree.
1586 *
1587 * <p>The latest state of Flutter's semantics tree is encoded in the given {@code buffer}. The
1588 * buffer is encoded by PlatformViewAndroid::UpdateSemantics, and the decode logic must be kept in
1589 * sync with that method's encoding logic.
1590 */
1591 void updateSemantics(
1592 @NonNull ByteBuffer buffer,
1593 @NonNull String[] strings,
1594 @NonNull ByteBuffer[] stringAttributeArgs) {
1595 ArrayList<SemanticsNode> updated = new ArrayList<>();
1596 while (buffer.hasRemaining()) {
1597 int id = buffer.getInt();
1598 SemanticsNode semanticsNode = getOrCreateSemanticsNode(id);
1599 semanticsNode.updateWith(buffer, strings, stringAttributeArgs);
1600 if (semanticsNode.hasFlag(Flag.IS_HIDDEN)) {
1601 continue;
1602 }
1603 if (semanticsNode.hasFlag(Flag.IS_FOCUSED)) {
1604 inputFocusedSemanticsNode = semanticsNode;
1605 }
1606 if (semanticsNode.hadPreviousConfig) {
1607 updated.add(semanticsNode);
1608 }
1609 if (semanticsNode.platformViewId != -1
1610 && !platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
1611 View embeddedView =
1612 platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
1613 if (embeddedView != null) {
1614 embeddedView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
1615 }
1616 }
1617 }
1618
1619 Set<SemanticsNode> visitedObjects = new HashSet<>();
1620 SemanticsNode rootObject = getRootSemanticsNode();
1621 List<SemanticsNode> newRoutes = new ArrayList<>();
1622 if (rootObject != null) {
1623 final float[] identity = new float[16];
1624 Matrix.setIdentityM(identity, 0);
1625 // In Android devices API 23 and above, the system nav bar can be placed on the left side
1626 // of the screen in landscape mode. We must handle the translation ourselves for the
1627 // a11y nodes.
1628 if (Build.VERSION.SDK_INT >= API_LEVELS.API_23) {
1629 boolean needsToApplyLeftCutoutInset = true;
1630 // In Android devices API 28 and above, the `layoutInDisplayCutoutMode` window attribute
1631 // can be set to allow overlapping content within the cutout area. Query the attribute
1632 // to figure out whether the content overlaps with the cutout and decide whether to
1633 // apply cutout inset.
1634 if (Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
1635 needsToApplyLeftCutoutInset = doesLayoutInDisplayCutoutModeRequireLeftInset();
1636 }
1637
1638 if (needsToApplyLeftCutoutInset) {
1639 WindowInsets insets = rootAccessibilityView.getRootWindowInsets();
1640 if (insets != null) {
1641 if (!lastLeftFrameInset.equals(insets.getSystemWindowInsetLeft())) {
1642 rootObject.globalGeometryDirty = true;
1643 rootObject.inverseTransformDirty = true;
1644 }
1645 lastLeftFrameInset = insets.getSystemWindowInsetLeft();
1646 Matrix.translateM(identity, 0, lastLeftFrameInset, 0, 0);
1647 }
1648 }
1649 }
1650 rootObject.updateRecursively(identity, visitedObjects, false);
1651 rootObject.collectRoutes(newRoutes);
1652 }
1653
1654 // Dispatch a TYPE_WINDOW_STATE_CHANGED event if the most recent route id changed from the
1655 // previously cached route id.
1656
1657 // Finds the last route that is not in the previous routes.
1658 SemanticsNode lastAdded = null;
1659 for (SemanticsNode semanticsNode : newRoutes) {
1660 if (!flutterNavigationStack.contains(semanticsNode.id)) {
1661 lastAdded = semanticsNode;
1662 }
1663 }
1664
1665 // If all the routes are in the previous route, get the last route.
1666 if (lastAdded == null && newRoutes.size() > 0) {
1667 lastAdded = newRoutes.get(newRoutes.size() - 1);
1668 }
1669
1670 // There are two cases if lastAdded != nil
1671 // 1. lastAdded is not in previous routes. In this case,
1672 // lastAdded.id != previousRouteId
1673 // 2. All new routes are in previous routes and
1674 // lastAdded = newRoutes.last.
1675 // In the first case, we need to announce new route. In the second case,
1676 // we need to announce if one list is shorter than the other.
1677 if (lastAdded != null
1678 && (lastAdded.id != previousRouteId || newRoutes.size() != flutterNavigationStack.size())) {
1679 previousRouteId = lastAdded.id;
1680 onWindowNameChange(lastAdded);
1681 }
1682 flutterNavigationStack.clear();
1683 for (SemanticsNode semanticsNode : newRoutes) {
1684 flutterNavigationStack.add(semanticsNode.id);
1685 }
1686
1687 Iterator<Map.Entry<Integer, SemanticsNode>> it = flutterSemanticsTree.entrySet().iterator();
1688 while (it.hasNext()) {
1689 Map.Entry<Integer, SemanticsNode> entry = it.next();
1690 SemanticsNode object = entry.getValue();
1691 if (!visitedObjects.contains(object)) {
1692 willRemoveSemanticsNode(object);
1693 it.remove();
1694 }
1695 }
1696
1697 // TODO(goderbauer): Send this event only once (!) for changed subtrees,
1698 // see https://github.com/flutter/flutter/issues/14534
1699 sendWindowContentChangeEvent(0);
1700
1701 for (SemanticsNode object : updated) {
1702 if (object.didScroll()) {
1703 AccessibilityEvent event =
1704 obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_SCROLLED);
1705
1706 // Android doesn't support unbound scrolling. So we pretend there is a large
1707 // bound (SCROLL_EXTENT_FOR_INFINITY), which you can never reach.
1708 float position = object.scrollPosition;
1709 float max = object.scrollExtentMax;
1710 if (Float.isInfinite(object.scrollExtentMax)) {
1711 max = SCROLL_EXTENT_FOR_INFINITY;
1712 if (position > SCROLL_POSITION_CAP_FOR_INFINITY) {
1713 position = SCROLL_POSITION_CAP_FOR_INFINITY;
1714 }
1715 }
1716 if (Float.isInfinite(object.scrollExtentMin)) {
1717 max += SCROLL_EXTENT_FOR_INFINITY;
1718 if (position < -SCROLL_POSITION_CAP_FOR_INFINITY) {
1719 position = -SCROLL_POSITION_CAP_FOR_INFINITY;
1720 }
1721 position += SCROLL_EXTENT_FOR_INFINITY;
1722 } else {
1723 max -= object.scrollExtentMin;
1724 position -= object.scrollExtentMin;
1725 }
1726
1727 if (object.hadAction(Action.SCROLL_UP) || object.hadAction(Action.SCROLL_DOWN)) {
1728 event.setScrollY((int) position);
1729 event.setMaxScrollY((int) max);
1730 } else if (object.hadAction(Action.SCROLL_LEFT) || object.hadAction(Action.SCROLL_RIGHT)) {
1731 event.setScrollX((int) position);
1732 event.setMaxScrollX((int) max);
1733 }
1734 if (object.scrollChildren > 0) {
1735 // We don't need to add 1 to the scroll index because TalkBack does this automagically.
1736 event.setItemCount(object.scrollChildren);
1737 event.setFromIndex(object.scrollIndex);
1738 int visibleChildren = 0;
1739 // handle hidden children at the beginning and end of the list.
1740 for (SemanticsNode child : object.childrenInHitTestOrder) {
1741 if (!child.hasFlag(Flag.IS_HIDDEN)) {
1742 visibleChildren += 1;
1743 }
1744 }
1745 if (BuildConfig.DEBUG) {
1746 if (object.scrollIndex + visibleChildren > object.scrollChildren) {
1747 Log.e(TAG, "Scroll index is out of bounds.");
1748 }
1749
1750 if (object.childrenInHitTestOrder.isEmpty()) {
1751 Log.e(TAG, "Had scrollChildren but no childrenInHitTestOrder");
1752 }
1753 }
1754 // The setToIndex should be the index of the last visible child. Because we counted all
1755 // children, including the first index we need to subtract one.
1756 //
1757 // [0, 1, 2, 3, 4, 5]
1758 // ^ ^
1759 // In the example above where 0 is the first visible index and 2 is the last, we will
1760 // count 3 total visible children. We then subtract one to get the correct last visible
1761 // index of 2.
1762 event.setToIndex(object.scrollIndex + visibleChildren - 1);
1763 }
1764 sendAccessibilityEvent(event);
1765 }
1766 if (object.hasFlag(Flag.IS_LIVE_REGION) && object.didChangeLabel()) {
1767 sendWindowContentChangeEvent(object.id);
1768 }
1769 if (accessibilityFocusedSemanticsNode != null
1770 && accessibilityFocusedSemanticsNode.id == object.id
1771 && !object.hadFlag(Flag.IS_SELECTED)
1772 && object.hasFlag(Flag.IS_SELECTED)) {
1773 AccessibilityEvent event =
1774 obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_SELECTED);
1775 event.getText().add(object.label);
1776 sendAccessibilityEvent(event);
1777 }
1778
1779 // If the object is the input-focused node, then tell the reader about it, but only if
1780 // it has changed since the last update.
1781 if (inputFocusedSemanticsNode != null
1782 && inputFocusedSemanticsNode.id == object.id
1783 && (lastInputFocusedSemanticsNode == null
1784 || lastInputFocusedSemanticsNode.id != inputFocusedSemanticsNode.id)) {
1785 lastInputFocusedSemanticsNode = inputFocusedSemanticsNode;
1786 sendAccessibilityEvent(
1787 obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_FOCUSED));
1788 } else if (inputFocusedSemanticsNode == null) {
1789 // There's no TYPE_VIEW_CLEAR_FOCUSED event, so if the current input focus becomes
1790 // null, then we just set the last one to null too, so that it sends the event again
1791 // when something regains focus.
1792 lastInputFocusedSemanticsNode = null;
1793 }
1794
1795 if (inputFocusedSemanticsNode != null
1796 && inputFocusedSemanticsNode.id == object.id
1797 && object.hadFlag(Flag.IS_TEXT_FIELD)
1798 && object.hasFlag(Flag.IS_TEXT_FIELD)
1799 // If we have a TextField that has InputFocus, we should avoid announcing it if something
1800 // else we track has a11y focus. This needs to still work when, e.g., IME has a11y focus
1801 // or the "PASTE" popup is used though.
1802 // See more discussion at https://github.com/flutter/flutter/issues/23180
1803 && (accessibilityFocusedSemanticsNode == null
1804 || (accessibilityFocusedSemanticsNode.id == inputFocusedSemanticsNode.id))) {
1805 String oldValue = object.previousValue != null ? object.previousValue : "";
1806 String newValue = object.value != null ? object.value : "";
1807 AccessibilityEvent event = createTextChangedEvent(object.id, oldValue, newValue);
1808 if (event != null) {
1809 sendAccessibilityEvent(event);
1810 }
1811
1812 if (object.previousTextSelectionBase != object.textSelectionBase
1813 || object.previousTextSelectionExtent != object.textSelectionExtent) {
1814 AccessibilityEvent selectionEvent =
1815 obtainAccessibilityEvent(
1816 object.id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
1817 selectionEvent.getText().add(newValue);
1818 selectionEvent.setFromIndex(object.textSelectionBase);
1819 selectionEvent.setToIndex(object.textSelectionExtent);
1820 selectionEvent.setItemCount(newValue.length());
1821 sendAccessibilityEvent(selectionEvent);
1822 }
1823 }
1824 }
1825 }
1826
1827 private AccessibilityEvent createTextChangedEvent(int id, String oldValue, String newValue) {
1828 AccessibilityEvent e = obtainAccessibilityEvent(id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
1829 e.setBeforeText(oldValue);
1830 e.getText().add(newValue);
1831
1832 int i;
1833 for (i = 0; i < oldValue.length() && i < newValue.length(); ++i) {
1834 if (oldValue.charAt(i) != newValue.charAt(i)) {
1835 break;
1836 }
1837 }
1838 if (i >= oldValue.length() && i >= newValue.length()) {
1839 return null; // Text did not change
1840 }
1841 int firstDifference = i;
1842 e.setFromIndex(firstDifference);
1843
1844 int oldIndex = oldValue.length() - 1;
1845 int newIndex = newValue.length() - 1;
1846 while (oldIndex >= firstDifference && newIndex >= firstDifference) {
1847 if (oldValue.charAt(oldIndex) != newValue.charAt(newIndex)) {
1848 break;
1849 }
1850 --oldIndex;
1851 --newIndex;
1852 }
1853 e.setRemovedCount(oldIndex - firstDifference + 1);
1854 e.setAddedCount(newIndex - firstDifference + 1);
1855
1856 return e;
1857 }
1858
1859 /**
1860 * Sends an accessibility event of the given {@code eventType} to Android's accessibility system
1861 * with the given {@code viewId} represented as the source of the event.
1862 *
1863 * <p>The given {@code viewId} may either belong to {@link #rootAccessibilityView}, or any Flutter
1864 * {@link SemanticsNode}.
1865 */
1866 @VisibleForTesting
1867 public void sendAccessibilityEvent(int viewId, int eventType) {
1868 if (!accessibilityManager.isEnabled()) {
1869 return;
1870 }
1871 sendAccessibilityEvent(obtainAccessibilityEvent(viewId, eventType));
1872 }
1873
1874 /**
1875 * Sends the given {@link AccessibilityEvent} to Android's accessibility system for a given
1876 * Flutter {@link SemanticsNode}.
1877 *
1878 * <p>This method should only be called for a Flutter {@link SemanticsNode}, not a traditional
1879 * Android {@code View}, i.e., {@link #rootAccessibilityView}.
1880 */
1881 private void sendAccessibilityEvent(@NonNull AccessibilityEvent event) {
1882 if (!accessibilityManager.isEnabled()) {
1883 return;
1884 }
1885 // See
1886 // https://developer.android.com/reference/android/view/View.html#sendAccessibilityEvent(int)
1887 // We just want the final part at this point, since the event parameter
1888 // has already been correctly populated.
1889 rootAccessibilityView.getParent().requestSendAccessibilityEvent(rootAccessibilityView, event);
1890 }
1891
1892 /**
1893 * Informs the TalkBack user about window name changes.
1894 *
1895 * <p>This method sets accessibility panel title if the API level >= 28, otherwise, it creates a
1896 * {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED} and sends the event to Android's
1897 * accessibility system. In both cases, TalkBack announces the label of the route and re-addjusts
1898 * the accessibility focus.
1899 *
1900 * <p>The given {@code route} should be a {@link SemanticsNode} that represents a navigation route
1901 * in the Flutter app.
1902 */
1903 private void onWindowNameChange(@NonNull SemanticsNode route) {
1904 String routeName = route.getRouteName();
1905 if (routeName == null) {
1906 // The routeName will be null when there is no semantics node that represnets namesRoute in
1907 // the scopeRoute. The TYPE_WINDOW_STATE_CHANGED only works the route name is not null and not
1908 // empty. Gives it a whitespace will make it focus the first semantics node without
1909 // pronouncing any word.
1910 //
1911 // The other way to trigger a focus change is to send a TYPE_VIEW_FOCUSED to the
1912 // rootAccessibilityView. However, it is less predictable which semantics node it will focus
1913 // next.
1914 routeName = " ";
1915 }
1916 if (Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
1917 setAccessibilityPaneTitle(routeName);
1918 } else {
1919 AccessibilityEvent event =
1920 obtainAccessibilityEvent(route.id, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
1921 event.getText().add(routeName);
1922 sendAccessibilityEvent(event);
1923 }
1924 }
1925
1926 @TargetApi(API_LEVELS.API_28)
1927 @RequiresApi(API_LEVELS.API_28)
1928 private void setAccessibilityPaneTitle(String title) {
1929 rootAccessibilityView.setAccessibilityPaneTitle(title);
1930 }
1931
1932 /**
1933 * Creates a {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} and sends the event to
1934 * Android's accessibility system.
1935 *
1936 * <p>It sets the content change types to {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE}
1937 * when supported by the API level.
1938 *
1939 * <p>The given {@code virtualViewId} should be a {@link SemanticsNode} below which the content
1940 * has changed.
1941 */
1942 private void sendWindowContentChangeEvent(int virtualViewId) {
1943 AccessibilityEvent event =
1944 obtainAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
1945 event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
1946 sendAccessibilityEvent(event);
1947 }
1948
1949 /**
1950 * Factory method that creates a new {@link AccessibilityEvent} that is configured to represent
1951 * the Flutter {@link SemanticsNode} represented by the given {@code virtualViewId}, categorized
1952 * as the given {@code eventType}.
1953 *
1954 * <p>This method should *only* be called for Flutter {@link SemanticsNode}s. It should *not* be
1955 * invoked to create an {@link AccessibilityEvent} for the {@link #rootAccessibilityView}.
1956 */
1957 private AccessibilityEvent obtainAccessibilityEvent(int virtualViewId, int eventType) {
1958 AccessibilityEvent event = obtainAccessibilityEvent(eventType);
1959 event.setPackageName(rootAccessibilityView.getContext().getPackageName());
1960 event.setSource(rootAccessibilityView, virtualViewId);
1961 return event;
1962 }
1963
1964 @VisibleForTesting
1965 public AccessibilityEvent obtainAccessibilityEvent(int eventType) {
1966 return AccessibilityEvent.obtain(eventType);
1967 }
1968
1969 /**
1970 * Reads the {@code layoutInDisplayCutoutMode} value from the window attribute and returns whether
1971 * a left cutout inset is required.
1972 *
1973 * <p>The {@code layoutInDisplayCutoutMode} is added after API level 28.
1974 */
1975 @TargetApi(API_LEVELS.API_28)
1976 @RequiresApi(API_LEVELS.API_28)
1977 private boolean doesLayoutInDisplayCutoutModeRequireLeftInset() {
1978 Context context = rootAccessibilityView.getContext();
1979 Activity activity = ViewUtils.getActivity(context);
1980 if (activity == null || activity.getWindow() == null) {
1981 // The activity is not visible, it does not matter whether to apply left inset
1982 // or not.
1983 return false;
1984 }
1985 int layoutInDisplayCutoutMode = activity.getWindow().getAttributes().layoutInDisplayCutoutMode;
1986 return layoutInDisplayCutoutMode
1987 == WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
1988 || layoutInDisplayCutoutMode
1989 == WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
1990 }
1991
1992 /**
1993 * Hook called just before a {@link SemanticsNode} is removed from the Android cache of Flutter's
1994 * semantics tree.
1995 */
1996 private void willRemoveSemanticsNode(SemanticsNode semanticsNodeToBeRemoved) {
1997 if (BuildConfig.DEBUG) {
1998 if (!flutterSemanticsTree.containsKey(semanticsNodeToBeRemoved.id)) {
1999 Log.e(TAG, "Attempted to remove a node that is not in the tree.");
2000 }
2001 if (flutterSemanticsTree.get(semanticsNodeToBeRemoved.id) != semanticsNodeToBeRemoved) {
2002 Log.e(TAG, "Flutter semantics tree failed to get expected node when searching by id.");
2003 }
2004 }
2005 // TODO(mattcarroll): should parent be set to "null" here? Changing the parent seems like the
2006 // behavior of a method called "removeSemanticsNode()". The same is true
2007 // for null'ing accessibilityFocusedSemanticsNode, inputFocusedSemanticsNode,
2008 // and hoveredObject. Is this a hook method or a command?
2009 semanticsNodeToBeRemoved.parent = null;
2010
2011 if (semanticsNodeToBeRemoved.platformViewId != -1
2012 && embeddedAccessibilityFocusedNodeId != null
2013 && accessibilityViewEmbedder.platformViewOfNode(embeddedAccessibilityFocusedNodeId)
2014 == platformViewsAccessibilityDelegate.getPlatformViewById(
2015 semanticsNodeToBeRemoved.platformViewId)) {
2016 // If the currently focused a11y node is within a platform view that is
2017 // getting removed: clear it's a11y focus.
2018 sendAccessibilityEvent(
2019 embeddedAccessibilityFocusedNodeId,
2020 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2021 embeddedAccessibilityFocusedNodeId = null;
2022 }
2023
2024 if (semanticsNodeToBeRemoved.platformViewId != -1) {
2025 View embeddedView =
2026 platformViewsAccessibilityDelegate.getPlatformViewById(
2027 semanticsNodeToBeRemoved.platformViewId);
2028 if (embeddedView != null) {
2029 embeddedView.setImportantForAccessibility(
2030 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
2031 }
2032 }
2033
2034 if (accessibilityFocusedSemanticsNode == semanticsNodeToBeRemoved) {
2035 sendAccessibilityEvent(
2036 accessibilityFocusedSemanticsNode.id,
2037 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2038 accessibilityFocusedSemanticsNode = null;
2039 }
2040
2041 if (inputFocusedSemanticsNode == semanticsNodeToBeRemoved) {
2042 inputFocusedSemanticsNode = null;
2043 }
2044
2045 if (hoveredObject == semanticsNodeToBeRemoved) {
2046 hoveredObject = null;
2047 }
2048 }
2049
2050 /**
2051 * Resets the {@code AccessibilityBridge}:
2052 *
2053 * <ul>
2054 * <li>Clears {@link #flutterSemanticsTree}, the Android cache of Flutter's semantics tree
2055 * <li>Releases focus on any active {@link #accessibilityFocusedSemanticsNode}
2056 * <li>Clears any hovered {@code SemanticsNode}
2057 * <li>Sends a {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} event
2058 * </ul>
2059 */
2060 // TODO(mattcarroll): under what conditions is this method expected to be invoked?
2061 public void reset() {
2062 flutterSemanticsTree.clear();
2063 if (accessibilityFocusedSemanticsNode != null) {
2064 sendAccessibilityEvent(
2065 accessibilityFocusedSemanticsNode.id,
2066 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2067 }
2068 accessibilityFocusedSemanticsNode = null;
2069 hoveredObject = null;
2070 sendWindowContentChangeEvent(0);
2071 }
2072
2073 /**
2074 * Listener that can be set on a {@link AccessibilityBridge}, which is invoked any time
2075 * accessibility is turned on/off, or touch exploration is turned on/off.
2076 */
2077 public interface OnAccessibilityChangeListener {
2078 void onAccessibilityChanged(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled);
2079 }
2080
2081 // Must match SemanticsActions in semantics.dart
2082 // https://github.com/flutter/engine/blob/main/lib/ui/semantics.dart
2083 public enum Action {
2084 TAP(1 << 0),
2085 LONG_PRESS(1 << 1),
2086 SCROLL_LEFT(1 << 2),
2087 SCROLL_RIGHT(1 << 3),
2088 SCROLL_UP(1 << 4),
2089 SCROLL_DOWN(1 << 5),
2090 INCREASE(1 << 6),
2091 DECREASE(1 << 7),
2092 SHOW_ON_SCREEN(1 << 8),
2093 MOVE_CURSOR_FORWARD_BY_CHARACTER(1 << 9),
2094 MOVE_CURSOR_BACKWARD_BY_CHARACTER(1 << 10),
2095 SET_SELECTION(1 << 11),
2096 COPY(1 << 12),
2097 CUT(1 << 13),
2098 PASTE(1 << 14),
2099 DID_GAIN_ACCESSIBILITY_FOCUS(1 << 15),
2100 DID_LOSE_ACCESSIBILITY_FOCUS(1 << 16),
2101 CUSTOM_ACTION(1 << 17),
2102 DISMISS(1 << 18),
2103 MOVE_CURSOR_FORWARD_BY_WORD(1 << 19),
2104 MOVE_CURSOR_BACKWARD_BY_WORD(1 << 20),
2105 SET_TEXT(1 << 21);
2106
2107 public final int value;
2108
2109 Action(int value) {
2110 this.value = value;
2111 }
2112 }
2113
2114 // Actions that are triggered by Android OS, as opposed to user-triggered actions.
2115 //
2116 // This int is intended to be use in a bitwise comparison.
2117 static int systemAction =
2118 Action.DID_GAIN_ACCESSIBILITY_FOCUS.value
2119 & Action.DID_LOSE_ACCESSIBILITY_FOCUS.value
2120 & Action.SHOW_ON_SCREEN.value;
2121
2122 // Must match SemanticsFlag in semantics.dart
2123 // https://github.com/flutter/engine/blob/main/lib/ui/semantics.dart
2124 /* Package */ enum Flag {
2125 HAS_CHECKED_STATE(1 << 0),
2126 IS_CHECKED(1 << 1),
2127 IS_SELECTED(1 << 2),
2128 IS_BUTTON(1 << 3),
2129 IS_TEXT_FIELD(1 << 4),
2130 IS_FOCUSED(1 << 5),
2131 HAS_ENABLED_STATE(1 << 6),
2132 IS_ENABLED(1 << 7),
2133 IS_IN_MUTUALLY_EXCLUSIVE_GROUP(1 << 8),
2134 IS_HEADER(1 << 9),
2135 IS_OBSCURED(1 << 10),
2136 SCOPES_ROUTE(1 << 11),
2137 NAMES_ROUTE(1 << 12),
2138 IS_HIDDEN(1 << 13),
2139 IS_IMAGE(1 << 14),
2140 IS_LIVE_REGION(1 << 15),
2141 HAS_TOGGLED_STATE(1 << 16),
2142 IS_TOGGLED(1 << 17),
2143 HAS_IMPLICIT_SCROLLING(1 << 18),
2144 IS_MULTILINE(1 << 19),
2145 IS_READ_ONLY(1 << 20),
2146 IS_FOCUSABLE(1 << 21),
2147 IS_LINK(1 << 22),
2148 IS_SLIDER(1 << 23),
2149 IS_KEYBOARD_KEY(1 << 24),
2150 IS_CHECK_STATE_MIXED(1 << 25),
2151 HAS_EXPANDED_STATE(1 << 26),
2152 IS_EXPANDED(1 << 27);
2153
2154 final int value;
2155
2156 Flag(int value) {
2157 this.value = value;
2158 }
2159 }
2160
2161 // Must match the enum defined in window.dart.
2162 private enum AccessibilityFeature {
2163 ACCESSIBLE_NAVIGATION(1 << 0),
2164 INVERT_COLORS(1 << 1), // NOT SUPPORTED
2165 DISABLE_ANIMATIONS(1 << 2),
2166 BOLD_TEXT(1 << 3), // NOT SUPPORTED
2167 REDUCE_MOTION(1 << 4), // NOT SUPPORTED
2168 HIGH_CONTRAST(1 << 5), // NOT SUPPORTED
2169 ON_OFF_SWITCH_LABELS(1 << 6); // NOT SUPPORTED
2170
2171 final int value;
2172
2173 AccessibilityFeature(int value) {
2174 this.value = value;
2175 }
2176 }
2177
2178 private enum TextDirection {
2179 UNKNOWN,
2180 LTR,
2181 RTL;
2182
2183 public static TextDirection fromInt(int value) {
2184 switch (value) {
2185 case 1:
2186 return RTL;
2187 case 2:
2188 return LTR;
2189 }
2190 return UNKNOWN;
2191 }
2192 }
2193
2194 /**
2195 * Accessibility action that is defined within a given Flutter application, as opposed to the
2196 * standard accessibility actions that are available in the Flutter framework.
2197 *
2198 * <p>Flutter and Android support a number of built-in accessibility actions. However, these
2199 * predefined actions are not always sufficient for a desired interaction. Android facilitates
2200 * custom accessibility actions,
2201 * https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction.
2202 * Flutter supports custom accessibility actions via {@code customSemanticsActions} within a
2203 * {@code Semantics} widget, https://api.flutter.dev/flutter/widgets/Semantics-class.html.
2204 *
2205 * <p>See the Android documentation for custom accessibility actions:
2206 * https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction
2207 *
2208 * <p>See the Flutter documentation for the Semantics widget:
2209 * https://api.flutter.dev/flutter/widgets/Semantics-class.html
2210 */
2211 private static class CustomAccessibilityAction {
2212 CustomAccessibilityAction() {}
2213
2214 // The ID of the custom action plus a minimum value so that the identifier
2215 // does not collide with existing Android accessibility actions. This ID
2216 // represents and Android resource ID, not a Flutter ID.
2217 private int resourceId = -1;
2218
2219 // The Flutter ID of this custom accessibility action. See Flutter's Semantics widget for
2220 // custom accessibility action definitions:
2221 // https://api.flutter.dev/flutter/widgets/Semantics-class.html
2222 private int id = -1;
2223
2224 // The ID of the standard Flutter accessibility action that this {@code
2225 // CustomAccessibilityAction}
2226 // overrides with a custom {@code label} and/or {@code hint}.
2227 private int overrideId = -1;
2228
2229 // The user presented value which is displayed in the local context menu.
2230 private String label;
2231
2232 // The text used in overridden standard actions.
2233 private String hint;
2234 }
2235
2236 // When adding a new StringAttributeType, the classes in these file must be
2237 // updated as well.
2238 // * engine/src/flutter/lib/ui/semantics.dart
2239 // * engine/src/flutter/lib/web_ui/lib/semantics.dart
2240 // * engine/src/flutter/lib/ui/semantics/string_attribute.h
2241
2242 private enum StringAttributeType {
2243 SPELLOUT,
2244 LOCALE,
2245 }
2246
2247 private static class StringAttribute {
2248 int start;
2249 int end;
2251 }
2252
2253 private static class SpellOutStringAttribute extends StringAttribute {}
2254
2255 private static class LocaleStringAttribute extends StringAttribute {
2256 String locale;
2257 }
2258
2259 /**
2260 * Flutter {@code SemanticsNode} represented in Java/Android.
2261 *
2262 * <p>Flutter maintains a semantics tree that is controlled by, but is independent of Flutter's
2263 * element tree, i.e., widgets/elements/render objects. Flutter's semantics tree must be cached on
2264 * the Android side so that Android can query any {@code SemanticsNode} at any time. This class
2265 * represents a single node in the semantics tree, and it is a Java representation of the
2266 * analogous concept within Flutter.
2267 *
2268 * <p>To see how this {@code SemanticsNode}'s fields correspond to Flutter's semantics system, see
2269 * semantics.dart: https://github.com/flutter/engine/blob/main/lib/ui/semantics.dart
2270 */
2271 private static class SemanticsNode {
2272 private static boolean nullableHasAncestor(
2273 SemanticsNode target, Predicate<SemanticsNode> tester) {
2274 return target != null && target.getAncestor(tester) != null;
2275 }
2276
2277 final AccessibilityBridge accessibilityBridge;
2278
2279 // Flutter ID of this {@code SemanticsNode}.
2280 private int id = -1;
2281
2282 private int flags;
2283 private int actions;
2284 private int maxValueLength;
2285 private int currentValueLength;
2286 private int textSelectionBase;
2287 private int textSelectionExtent;
2288 private int platformViewId;
2289 private int scrollChildren;
2290 private int scrollIndex;
2291 private float scrollPosition;
2292 private float scrollExtentMax;
2293 private float scrollExtentMin;
2294 private String identifier;
2295 private String label;
2296 private List<StringAttribute> labelAttributes;
2297 private String value;
2298 private List<StringAttribute> valueAttributes;
2299 private String increasedValue;
2300 private List<StringAttribute> increasedValueAttributes;
2301 private String decreasedValue;
2302 private List<StringAttribute> decreasedValueAttributes;
2303 private String hint;
2304 private List<StringAttribute> hintAttributes;
2305
2306 // The textual description of the backing widget's tooltip.
2307 //
2308 // The tooltip is attached through AccessibilityNodeInfo.setTooltipText if
2309 // API level >= 28; otherwise, this is attached to the end of content description.
2310 @Nullable private String tooltip;
2311
2312 // The id of the sibling node that is before this node in traversal
2313 // order.
2314 //
2315 // The child order alone does not guarantee the TalkBack focus traversal
2316 // order. The AccessibilityNodeInfo.setTraversalAfter must be called with
2317 // its previous sibling to determine the focus traversal order.
2318 //
2319 // This property is updated in AccessibilityBridge.updateRecursively,
2320 // which is called at the end of every semantics update, and it is used in
2321 // AccessibilityBridge.createAccessibilityNodeInfo to set the "traversal
2322 // after" of this node.
2323 private int previousNodeId = -1;
2324
2325 // See Flutter's {@code SemanticsNode#textDirection}.
2326 private TextDirection textDirection;
2327
2328 private boolean hadPreviousConfig = false;
2329 private int previousFlags;
2330 private int previousActions;
2331 private int previousTextSelectionBase;
2332 private int previousTextSelectionExtent;
2333 private float previousScrollPosition;
2334 private float previousScrollExtentMax;
2335 private float previousScrollExtentMin;
2336 private String previousValue;
2337 private String previousLabel;
2338
2339 private float left;
2340 private float top;
2341 private float right;
2342 private float bottom;
2343 private float[] transform;
2344
2345 private SemanticsNode parent;
2346 private List<SemanticsNode> childrenInTraversalOrder = new ArrayList<>();
2347 private List<SemanticsNode> childrenInHitTestOrder = new ArrayList<>();
2348 private List<CustomAccessibilityAction> customAccessibilityActions;
2349 private CustomAccessibilityAction onTapOverride;
2350 private CustomAccessibilityAction onLongPressOverride;
2351
2352 private boolean inverseTransformDirty = true;
2353 private float[] inverseTransform;
2354
2355 private boolean globalGeometryDirty = true;
2356 private float[] globalTransform;
2357 private Rect globalRect;
2358
2359 SemanticsNode(@NonNull AccessibilityBridge accessibilityBridge) {
2360 this.accessibilityBridge = accessibilityBridge;
2361 }
2362
2363 /**
2364 * Returns the ancestor of this {@code SemanticsNode} for which {@link Predicate#test(Object)}
2365 * returns true, or null if no such ancestor exists.
2366 */
2367 private SemanticsNode getAncestor(Predicate<SemanticsNode> tester) {
2368 SemanticsNode nextAncestor = parent;
2369 while (nextAncestor != null) {
2370 if (tester.test(nextAncestor)) {
2371 return nextAncestor;
2372 }
2373 nextAncestor = nextAncestor.parent;
2374 }
2375 return null;
2376 }
2377
2378 /**
2379 * Returns true if the given {@code action} is supported by this {@code SemanticsNode}.
2380 *
2381 * <p>This method only applies to this {@code SemanticsNode} and does not implicitly search its
2382 * children.
2383 */
2384 private boolean hasAction(@NonNull Action action) {
2385 return (actions & action.value) != 0;
2386 }
2387
2388 /**
2389 * Returns true if the given {@code action} was supported by the immediately previous version of
2390 * this {@code SemanticsNode}.
2391 */
2392 private boolean hadAction(@NonNull Action action) {
2393 return (previousActions & action.value) != 0;
2394 }
2395
2396 private boolean hasFlag(@NonNull Flag flag) {
2397 return (flags & flag.value) != 0;
2398 }
2399
2400 private boolean hadFlag(@NonNull Flag flag) {
2401 if (BuildConfig.DEBUG && !hadPreviousConfig) {
2402 Log.e(TAG, "Attempted to check hadFlag but had no previous config.");
2403 }
2404 return (previousFlags & flag.value) != 0;
2405 }
2406
2407 private boolean didScroll() {
2408 return !Float.isNaN(scrollPosition)
2409 && !Float.isNaN(previousScrollPosition)
2410 && previousScrollPosition != scrollPosition;
2411 }
2412
2413 private boolean didChangeLabel() {
2414 if (label == null && previousLabel == null) {
2415 return false;
2416 }
2417 return label == null || previousLabel == null || !label.equals(previousLabel);
2418 }
2419
2420 private void log(@NonNull String indent, boolean recursive) {
2421 if (BuildConfig.DEBUG) {
2422 Log.i(
2423 TAG,
2424 indent
2425 + "SemanticsNode id="
2426 + id
2427 + " identifier="
2428 + identifier
2429 + " label="
2430 + label
2431 + " actions="
2432 + actions
2433 + " flags="
2434 + flags
2435 + "\n"
2436 + indent
2437 + " +-- textDirection="
2438 + textDirection
2439 + "\n"
2440 + indent
2441 + " +-- rect.ltrb=("
2442 + left
2443 + ", "
2444 + top
2445 + ", "
2446 + right
2447 + ", "
2448 + bottom
2449 + ")\n"
2450 + indent
2451 + " +-- transform="
2452 + Arrays.toString(transform)
2453 + "\n");
2454 if (recursive) {
2455 String childIndent = indent + " ";
2456 for (SemanticsNode child : childrenInTraversalOrder) {
2457 child.log(childIndent, recursive);
2458 }
2459 }
2460 }
2461 }
2462
2463 private void updateWith(
2464 @NonNull ByteBuffer buffer,
2465 @NonNull String[] strings,
2466 @NonNull ByteBuffer[] stringAttributeArgs) {
2467 hadPreviousConfig = true;
2468 previousValue = value;
2469 previousLabel = label;
2470 previousFlags = flags;
2471 previousActions = actions;
2472 previousTextSelectionBase = textSelectionBase;
2473 previousTextSelectionExtent = textSelectionExtent;
2474 previousScrollPosition = scrollPosition;
2475 previousScrollExtentMax = scrollExtentMax;
2476 previousScrollExtentMin = scrollExtentMin;
2477
2478 flags = buffer.getInt();
2479 actions = buffer.getInt();
2480 maxValueLength = buffer.getInt();
2481 currentValueLength = buffer.getInt();
2482 textSelectionBase = buffer.getInt();
2483 textSelectionExtent = buffer.getInt();
2484 platformViewId = buffer.getInt();
2485 scrollChildren = buffer.getInt();
2486 scrollIndex = buffer.getInt();
2487 scrollPosition = buffer.getFloat();
2488 scrollExtentMax = buffer.getFloat();
2489 scrollExtentMin = buffer.getFloat();
2490
2491 int stringIndex = buffer.getInt();
2492
2493 identifier = stringIndex == -1 ? null : strings[stringIndex];
2494 stringIndex = buffer.getInt();
2495
2496 label = stringIndex == -1 ? null : strings[stringIndex];
2497
2498 labelAttributes = getStringAttributesFromBuffer(buffer, stringAttributeArgs);
2499
2500 stringIndex = buffer.getInt();
2501 value = stringIndex == -1 ? null : strings[stringIndex];
2502
2503 valueAttributes = getStringAttributesFromBuffer(buffer, stringAttributeArgs);
2504
2505 stringIndex = buffer.getInt();
2506 increasedValue = stringIndex == -1 ? null : strings[stringIndex];
2507
2508 increasedValueAttributes = getStringAttributesFromBuffer(buffer, stringAttributeArgs);
2509
2510 stringIndex = buffer.getInt();
2511 decreasedValue = stringIndex == -1 ? null : strings[stringIndex];
2512
2513 decreasedValueAttributes = getStringAttributesFromBuffer(buffer, stringAttributeArgs);
2514
2515 stringIndex = buffer.getInt();
2516 hint = stringIndex == -1 ? null : strings[stringIndex];
2517
2518 hintAttributes = getStringAttributesFromBuffer(buffer, stringAttributeArgs);
2519
2520 stringIndex = buffer.getInt();
2521 tooltip = stringIndex == -1 ? null : strings[stringIndex];
2522
2523 textDirection = TextDirection.fromInt(buffer.getInt());
2524
2525 left = buffer.getFloat();
2526 top = buffer.getFloat();
2527 right = buffer.getFloat();
2528 bottom = buffer.getFloat();
2529
2530 if (transform == null) {
2531 transform = new float[16];
2532 }
2533 for (int i = 0; i < 16; ++i) {
2534 transform[i] = buffer.getFloat();
2535 }
2536 inverseTransformDirty = true;
2537 globalGeometryDirty = true;
2538
2539 final int childCount = buffer.getInt();
2540 childrenInTraversalOrder.clear();
2541 childrenInHitTestOrder.clear();
2542 for (int i = 0; i < childCount; ++i) {
2543 SemanticsNode child = accessibilityBridge.getOrCreateSemanticsNode(buffer.getInt());
2544 child.parent = this;
2545 childrenInTraversalOrder.add(child);
2546 }
2547 for (int i = 0; i < childCount; ++i) {
2548 SemanticsNode child = accessibilityBridge.getOrCreateSemanticsNode(buffer.getInt());
2549 child.parent = this;
2550 childrenInHitTestOrder.add(child);
2551 }
2552
2553 final int actionCount = buffer.getInt();
2554 if (actionCount == 0) {
2555 customAccessibilityActions = null;
2556 } else {
2557 if (customAccessibilityActions == null)
2558 customAccessibilityActions = new ArrayList<>(actionCount);
2559 else customAccessibilityActions.clear();
2560
2561 for (int i = 0; i < actionCount; i++) {
2562 CustomAccessibilityAction action =
2563 accessibilityBridge.getOrCreateAccessibilityAction(buffer.getInt());
2564 if (action.overrideId == Action.TAP.value) {
2565 onTapOverride = action;
2566 } else if (action.overrideId == Action.LONG_PRESS.value) {
2567 onLongPressOverride = action;
2568 } else {
2569 // If we receive a different overrideId it means that we were passed
2570 // a standard action to override that we don't yet support.
2571 if (BuildConfig.DEBUG && action.overrideId != -1) {
2572 Log.e(TAG, "Expected action.overrideId to be -1.");
2573 }
2574 customAccessibilityActions.add(action);
2575 }
2576 customAccessibilityActions.add(action);
2577 }
2578 }
2579 }
2580
2581 private List<StringAttribute> getStringAttributesFromBuffer(
2582 @NonNull ByteBuffer buffer, @NonNull ByteBuffer[] stringAttributeArgs) {
2583 final int attributesCount = buffer.getInt();
2584 if (attributesCount == -1) {
2585 return null;
2586 }
2587 final List<StringAttribute> result = new ArrayList<>(attributesCount);
2588 for (int i = 0; i < attributesCount; ++i) {
2589 final int start = buffer.getInt();
2590 final int end = buffer.getInt();
2591 final StringAttributeType type = StringAttributeType.values()[buffer.getInt()];
2592 switch (type) {
2593 case SPELLOUT:
2594 {
2595 // Pops the -1 size.
2596 buffer.getInt();
2597 SpellOutStringAttribute attribute = new SpellOutStringAttribute();
2598 attribute.start = start;
2599 attribute.end = end;
2600 attribute.type = type;
2601 result.add(attribute);
2602 break;
2603 }
2604 case LOCALE:
2605 {
2606 final int argsIndex = buffer.getInt();
2607 final ByteBuffer args = stringAttributeArgs[argsIndex];
2608 LocaleStringAttribute attribute = new LocaleStringAttribute();
2609 attribute.start = start;
2610 attribute.end = end;
2611 attribute.type = type;
2612 attribute.locale = Charset.forName("UTF-8").decode(args).toString();
2613 result.add(attribute);
2614 break;
2615 }
2616 default:
2617 break;
2618 }
2619 }
2620 return result;
2621 }
2622
2623 private void ensureInverseTransform() {
2624 if (!inverseTransformDirty) {
2625 return;
2626 }
2627 inverseTransformDirty = false;
2628 if (inverseTransform == null) {
2629 inverseTransform = new float[16];
2630 }
2631 if (!Matrix.invertM(inverseTransform, 0, transform, 0)) {
2632 Arrays.fill(inverseTransform, 0);
2633 }
2634 }
2635
2636 private Rect getGlobalRect() {
2637 if (BuildConfig.DEBUG && globalGeometryDirty) {
2638 Log.e(TAG, "Attempted to getGlobalRect with a dirty geometry.");
2639 }
2640 return globalRect;
2641 }
2642
2643 /**
2644 * Hit tests {@code point} to find the deepest focusable node in the node tree at that point.
2645 *
2646 * @param point The point to hit test against this node.
2647 * @param stopAtPlatformView Whether to return a platform view if found, regardless of whether
2648 * or not it is focusable.
2649 * @return The found node, or null if no relevant node was found at the given point.
2650 */
2651 private SemanticsNode hitTest(float[] point, boolean stopAtPlatformView) {
2652 final float w = point[3];
2653 final float x = point[0] / w;
2654 final float y = point[1] / w;
2655 if (x < left || x >= right || y < top || y >= bottom) return null;
2656 final float[] transformedPoint = new float[4];
2657 for (SemanticsNode child : childrenInHitTestOrder) {
2658 if (child.hasFlag(Flag.IS_HIDDEN)) {
2659 continue;
2660 }
2661 child.ensureInverseTransform();
2662 Matrix.multiplyMV(transformedPoint, 0, child.inverseTransform, 0, point, 0);
2663 final SemanticsNode result = child.hitTest(transformedPoint, stopAtPlatformView);
2664 if (result != null) {
2665 return result;
2666 }
2667 }
2668 final boolean foundPlatformView = stopAtPlatformView && platformViewId != -1;
2669 return isFocusable() || foundPlatformView ? this : null;
2670 }
2671
2672 // TODO(goderbauer): This should be decided by the framework once we have more information
2673 // about focusability there.
2674 private boolean isFocusable() {
2675 // We enforce in the framework that no other useful semantics are merged with these
2676 // nodes.
2677 if (hasFlag(Flag.SCOPES_ROUTE)) {
2678 return false;
2679 }
2680 if (hasFlag(Flag.IS_FOCUSABLE)) {
2681 return true;
2682 }
2683 // If not explicitly set as focusable, then use our legacy
2684 // algorithm. Once all focusable widgets have a Focus widget, then
2685 // this won't be needed.
2686 return (actions & ~SCROLLABLE_ACTIONS) != 0
2687 || (flags & FOCUSABLE_FLAGS) != 0
2688 || (label != null && !label.isEmpty())
2689 || (value != null && !value.isEmpty())
2690 || (hint != null && !hint.isEmpty());
2691 }
2692
2693 private void collectRoutes(List<SemanticsNode> edges) {
2694 if (hasFlag(Flag.SCOPES_ROUTE)) {
2695 edges.add(this);
2696 }
2697 for (SemanticsNode child : childrenInTraversalOrder) {
2698 child.collectRoutes(edges);
2699 }
2700 }
2701
2702 private String getRouteName() {
2703 // Returns the first non-null and non-empty semantic label of a child
2704 // with an NamesRoute flag. Otherwise returns null.
2705 if (hasFlag(Flag.NAMES_ROUTE)) {
2706 if (label != null && !label.isEmpty()) {
2707 return label;
2708 }
2709 }
2710 for (SemanticsNode child : childrenInTraversalOrder) {
2711 String newName = child.getRouteName();
2712 if (newName != null && !newName.isEmpty()) {
2713 return newName;
2714 }
2715 }
2716 return null;
2717 }
2718
2719 private void updateRecursively(
2720 float[] ancestorTransform, Set<SemanticsNode> visitedObjects, boolean forceUpdate) {
2721 visitedObjects.add(this);
2722
2723 if (globalGeometryDirty) {
2724 forceUpdate = true;
2725 }
2726
2727 if (forceUpdate) {
2728 if (globalTransform == null) {
2729 globalTransform = new float[16];
2730 }
2731 if (transform == null) {
2732 if (BuildConfig.DEBUG) {
2733 Log.e(TAG, "transform has not been initialized for id = " + id);
2734 accessibilityBridge.getRootSemanticsNode().log("Semantics tree:", true);
2735 }
2736 transform = new float[16];
2737 }
2738 Matrix.multiplyMM(globalTransform, 0, ancestorTransform, 0, transform, 0);
2739
2740 final float[] sample = new float[4];
2741 sample[2] = 0;
2742 sample[3] = 1;
2743
2744 final float[] point1 = new float[4];
2745 final float[] point2 = new float[4];
2746 final float[] point3 = new float[4];
2747 final float[] point4 = new float[4];
2748
2749 sample[0] = left;
2750 sample[1] = top;
2751 transformPoint(point1, globalTransform, sample);
2752
2753 sample[0] = right;
2754 sample[1] = top;
2755 transformPoint(point2, globalTransform, sample);
2756
2757 sample[0] = right;
2758 sample[1] = bottom;
2759 transformPoint(point3, globalTransform, sample);
2760
2761 sample[0] = left;
2762 sample[1] = bottom;
2763 transformPoint(point4, globalTransform, sample);
2764
2765 if (globalRect == null) globalRect = new Rect();
2766
2767 globalRect.set(
2768 Math.round(min(point1[0], point2[0], point3[0], point4[0])),
2769 Math.round(min(point1[1], point2[1], point3[1], point4[1])),
2770 Math.round(max(point1[0], point2[0], point3[0], point4[0])),
2771 Math.round(max(point1[1], point2[1], point3[1], point4[1])));
2772
2773 globalGeometryDirty = false;
2774 }
2775
2776 if (BuildConfig.DEBUG) {
2777 if (globalTransform == null) {
2778 Log.e(TAG, "Expected globalTransform to not be null.");
2779 }
2780 if (globalRect == null) {
2781 Log.e(TAG, "Expected globalRect to not be null.");
2782 }
2783 }
2784
2785 int previousNodeId = -1;
2786 for (SemanticsNode child : childrenInTraversalOrder) {
2787 child.previousNodeId = previousNodeId;
2788 previousNodeId = child.id;
2789 child.updateRecursively(globalTransform, visitedObjects, forceUpdate);
2790 }
2791 }
2792
2793 private void transformPoint(float[] result, float[] transform, float[] point) {
2794 Matrix.multiplyMV(result, 0, transform, 0, point, 0);
2795 final float w = result[3];
2796 result[0] /= w;
2797 result[1] /= w;
2798 result[2] /= w;
2799 result[3] = 0;
2800 }
2801
2802 private float min(float a, float b, float c, float d) {
2803 return Math.min(a, Math.min(b, Math.min(c, d)));
2804 }
2805
2806 private float max(float a, float b, float c, float d) {
2807 return Math.max(a, Math.max(b, Math.max(c, d)));
2808 }
2809
2810 private CharSequence getValue() {
2811 return createSpannableString(value, valueAttributes);
2812 }
2813
2814 private CharSequence getLabel() {
2815 return createSpannableString(label, labelAttributes);
2816 }
2817
2818 private CharSequence getHint() {
2819 return createSpannableString(hint, hintAttributes);
2820 }
2821
2822 private CharSequence getValueLabelHint() {
2823 CharSequence[] array = new CharSequence[] {getValue(), getLabel(), getHint()};
2824 CharSequence result = null;
2825 for (CharSequence word : array) {
2826 if (word != null && word.length() > 0) {
2827 if (result == null || result.length() == 0) {
2828 result = word;
2829 } else {
2830 result = TextUtils.concat(result, ", ", word);
2831 }
2832 }
2833 }
2834 return result;
2835 }
2836
2837 private CharSequence getTextFieldHint() {
2838 CharSequence[] array = new CharSequence[] {getLabel(), getHint()};
2839 CharSequence result = null;
2840 for (CharSequence word : array) {
2841 if (word != null && word.length() > 0) {
2842 if (result == null || result.length() == 0) {
2843 result = word;
2844 } else {
2845 result = TextUtils.concat(result, ", ", word);
2846 }
2847 }
2848 }
2849 return result;
2850 }
2851
2852 private SpannableString createSpannableString(String string, List<StringAttribute> attributes) {
2853 if (string == null) {
2854 return null;
2855 }
2856 final SpannableString spannableString = new SpannableString(string);
2857 if (attributes != null) {
2858 for (StringAttribute attribute : attributes) {
2859 switch (attribute.type) {
2860 case SPELLOUT:
2861 {
2862 final TtsSpan ttsSpan = new TtsSpan.Builder<>(TtsSpan.TYPE_VERBATIM).build();
2863 spannableString.setSpan(ttsSpan, attribute.start, attribute.end, 0);
2864 break;
2865 }
2866 case LOCALE:
2867 {
2868 LocaleStringAttribute localeAttribute = (LocaleStringAttribute) attribute;
2869 Locale locale = Locale.forLanguageTag(localeAttribute.locale);
2870 final LocaleSpan localeSpan = new LocaleSpan(locale);
2871 spannableString.setSpan(localeSpan, attribute.start, attribute.end, 0);
2872 break;
2873 }
2874 }
2875 }
2876 }
2877 return spannableString;
2878 }
2879 }
2880
2881 /**
2882 * Delegates handling of {@link android.view.ViewParent#requestSendAccessibilityEvent} to the
2883 * accessibility bridge.
2884 *
2885 * <p>This is used by embedded platform views to propagate accessibility events from their view
2886 * hierarchy to the accessibility bridge.
2887 *
2888 * <p>As the embedded view doesn't have to be the only View in the embedded hierarchy (it can have
2889 * child views) and the event might have been originated from any view in this hierarchy, this
2890 * method gets both a reference to the embedded platform view, and a reference to the view from
2891 * its hierarchy that sent the event.
2892 *
2893 * @param embeddedView the embedded platform view for which the event is delegated
2894 * @param eventOrigin the view in the embedded view's hierarchy that sent the event.
2895 * @return True if the event was sent.
2896 */
2897 // AccessibilityEvent has many irrelevant cases that would be confusing to list.
2898 @SuppressLint("SwitchIntDef")
2899 public boolean externalViewRequestSendAccessibilityEvent(
2900 View embeddedView, View eventOrigin, AccessibilityEvent event) {
2901 if (!accessibilityViewEmbedder.requestSendAccessibilityEvent(
2902 embeddedView, eventOrigin, event)) {
2903 return false;
2904 }
2905 Integer virtualNodeId = accessibilityViewEmbedder.getRecordFlutterId(embeddedView, event);
2906 if (virtualNodeId == null) {
2907 return false;
2908 }
2909 switch (event.getEventType()) {
2910 case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
2911 hoveredObject = null;
2912 break;
2913 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
2914 embeddedAccessibilityFocusedNodeId = virtualNodeId;
2915 accessibilityFocusedSemanticsNode = null;
2916 break;
2917 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
2918 embeddedInputFocusedNodeId = null;
2919 embeddedAccessibilityFocusedNodeId = null;
2920 break;
2921 case AccessibilityEvent.TYPE_VIEW_FOCUSED:
2922 embeddedInputFocusedNodeId = virtualNodeId;
2923 inputFocusedSemanticsNode = null;
2924 break;
2925 }
2926 return true;
2927 }
2928}
m reset()
static bool left(const SkPoint &p0, const SkPoint &p1)
static bool right(const SkPoint &p0, const SkPoint &p1)
void add(sk_sp< SkIDChangeListener > listener) SK_EXCLUDES(fMutex)
AccessibilityBridge( @NonNull View rootAccessibilityView, @NonNull AccessibilityChannel accessibilityChannel, @NonNull AccessibilityManager accessibilityManager, @NonNull ContentResolver contentResolver, @NonNull PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate)
AccessibilityBridge( @NonNull View rootAccessibilityView, @NonNull AccessibilityChannel accessibilityChannel, @NonNull AccessibilityManager accessibilityManager, @NonNull ContentResolver contentResolver, @NonNull AccessibilityViewEmbedder accessibilityViewEmbedder, @NonNull PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate)
static SkString identifier(const FontFamilyDesc &family, const FontDesc &font)
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE auto & d
Definition main.cc:19
static bool b
struct MyStruct a[10]
FlutterSemanticsFlag flag
FlutterSemanticsFlag flags
glong glong end
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
FlKeyEvent * event
static const uint8_t buffer[]
uint8_t value
GAsyncResult * result
uint32_t * target
static float max(float r, float g, float b)
Definition hsl.cpp:49
static float min(float r, float g, float b)
Definition hsl.cpp:48
size_t length
union flutter::testing::@2838::KeyboardChange::@76 content
Win32Message message
double y
double x
Optional< SkRect > bounds
Definition SkRecords.h:189
void Log(const char *format,...) SK_PRINTF_LIKE(1
Definition build.py:1
Build(configs, env, options)
Definition build.py:232
intptr_t word
Definition globals.h:500
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir Path to the cache directory This is different from the persistent_cache_path in embedder which is used for Skia shader cache icu native lib Path to the library file that exports the ICU data vm service The hostname IP address on which the Dart VM Service should be served If not defaults to or::depending on whether ipv6 is specified vm service A custom Dart VM Service port The default is to pick a randomly available open port disable vm Disable the Dart VM Service The Dart VM Service is never available in release mode disable vm service Disable mDNS Dart VM Service publication Bind to the IPv6 localhost address for the Dart VM Service Ignored if vm service host is set endless trace Enable an endless trace buffer The default is a ring buffer This is useful when very old events need to viewed For during application launch Memory usage will continue to grow indefinitely however route
Definition switches.h:137
TRect< Scalar > Rect
Definition rect.h:746
static SkColor4f transform(SkColor4f c, SkColorSpace *src, SkColorSpace *dst)
Definition p3.cpp:47
SkScalar w
#define TAG()
const uintptr_t id