Flutter Engine
The Flutter Engine
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 FOCUS(1 << 22);
2107
2108 public final int value;
2109
2110 Action(int value) {
2111 this.value = value;
2112 }
2113 }
2114
2115 // Actions that are triggered by Android OS, as opposed to user-triggered actions.
2116 //
2117 // This int is intended to be use in a bitwise comparison.
2118 static int systemAction =
2119 Action.DID_GAIN_ACCESSIBILITY_FOCUS.value
2120 & Action.DID_LOSE_ACCESSIBILITY_FOCUS.value
2121 & Action.SHOW_ON_SCREEN.value;
2122
2123 // Must match SemanticsFlag in semantics.dart
2124 // https://github.com/flutter/engine/blob/main/lib/ui/semantics.dart
2125 /* Package */ enum Flag {
2126 HAS_CHECKED_STATE(1 << 0),
2127 IS_CHECKED(1 << 1),
2128 IS_SELECTED(1 << 2),
2129 IS_BUTTON(1 << 3),
2130 IS_TEXT_FIELD(1 << 4),
2131 IS_FOCUSED(1 << 5),
2132 HAS_ENABLED_STATE(1 << 6),
2133 IS_ENABLED(1 << 7),
2134 IS_IN_MUTUALLY_EXCLUSIVE_GROUP(1 << 8),
2135 IS_HEADER(1 << 9),
2136 IS_OBSCURED(1 << 10),
2137 SCOPES_ROUTE(1 << 11),
2138 NAMES_ROUTE(1 << 12),
2139 IS_HIDDEN(1 << 13),
2140 IS_IMAGE(1 << 14),
2141 IS_LIVE_REGION(1 << 15),
2142 HAS_TOGGLED_STATE(1 << 16),
2143 IS_TOGGLED(1 << 17),
2144 HAS_IMPLICIT_SCROLLING(1 << 18),
2145 IS_MULTILINE(1 << 19),
2146 IS_READ_ONLY(1 << 20),
2147 IS_FOCUSABLE(1 << 21),
2148 IS_LINK(1 << 22),
2149 IS_SLIDER(1 << 23),
2150 IS_KEYBOARD_KEY(1 << 24),
2151 IS_CHECK_STATE_MIXED(1 << 25),
2152 HAS_EXPANDED_STATE(1 << 26),
2153 IS_EXPANDED(1 << 27);
2154
2155 final int value;
2156
2157 Flag(int value) {
2158 this.value = value;
2159 }
2160 }
2161
2162 // Must match the enum defined in window.dart.
2163 private enum AccessibilityFeature {
2164 ACCESSIBLE_NAVIGATION(1 << 0),
2165 INVERT_COLORS(1 << 1), // NOT SUPPORTED
2166 DISABLE_ANIMATIONS(1 << 2),
2167 BOLD_TEXT(1 << 3), // NOT SUPPORTED
2168 REDUCE_MOTION(1 << 4), // NOT SUPPORTED
2169 HIGH_CONTRAST(1 << 5), // NOT SUPPORTED
2170 ON_OFF_SWITCH_LABELS(1 << 6); // NOT SUPPORTED
2171
2172 final int value;
2173
2174 AccessibilityFeature(int value) {
2175 this.value = value;
2176 }
2177 }
2178
2179 private enum TextDirection {
2180 UNKNOWN,
2181 LTR,
2182 RTL;
2183
2184 public static TextDirection fromInt(int value) {
2185 switch (value) {
2186 case 1:
2187 return RTL;
2188 case 2:
2189 return LTR;
2190 }
2191 return UNKNOWN;
2192 }
2193 }
2194
2195 /**
2196 * Accessibility action that is defined within a given Flutter application, as opposed to the
2197 * standard accessibility actions that are available in the Flutter framework.
2198 *
2199 * <p>Flutter and Android support a number of built-in accessibility actions. However, these
2200 * predefined actions are not always sufficient for a desired interaction. Android facilitates
2201 * custom accessibility actions,
2202 * https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction.
2203 * Flutter supports custom accessibility actions via {@code customSemanticsActions} within a
2204 * {@code Semantics} widget, https://api.flutter.dev/flutter/widgets/Semantics-class.html.
2205 *
2206 * <p>See the Android documentation for custom accessibility actions:
2207 * https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo.AccessibilityAction
2208 *
2209 * <p>See the Flutter documentation for the Semantics widget:
2210 * https://api.flutter.dev/flutter/widgets/Semantics-class.html
2211 */
2212 private static class CustomAccessibilityAction {
2213 CustomAccessibilityAction() {}
2214
2215 // The ID of the custom action plus a minimum value so that the identifier
2216 // does not collide with existing Android accessibility actions. This ID
2217 // represents and Android resource ID, not a Flutter ID.
2218 private int resourceId = -1;
2219
2220 // The Flutter ID of this custom accessibility action. See Flutter's Semantics widget for
2221 // custom accessibility action definitions:
2222 // https://api.flutter.dev/flutter/widgets/Semantics-class.html
2223 private int id = -1;
2224
2225 // The ID of the standard Flutter accessibility action that this {@code
2226 // CustomAccessibilityAction}
2227 // overrides with a custom {@code label} and/or {@code hint}.
2228 private int overrideId = -1;
2229
2230 // The user presented value which is displayed in the local context menu.
2231 private String label;
2232
2233 // The text used in overridden standard actions.
2234 private String hint;
2235 }
2236
2237 // When adding a new StringAttributeType, the classes in these file must be
2238 // updated as well.
2239 // * engine/src/flutter/lib/ui/semantics.dart
2240 // * engine/src/flutter/lib/web_ui/lib/semantics.dart
2241 // * engine/src/flutter/lib/ui/semantics/string_attribute.h
2242
2243 private enum StringAttributeType {
2244 SPELLOUT,
2245 LOCALE,
2246 }
2247
2248 private static class StringAttribute {
2249 int start;
2250 int end;
2252 }
2253
2254 private static class SpellOutStringAttribute extends StringAttribute {}
2255
2256 private static class LocaleStringAttribute extends StringAttribute {
2257 String locale;
2258 }
2259
2260 /**
2261 * Flutter {@code SemanticsNode} represented in Java/Android.
2262 *
2263 * <p>Flutter maintains a semantics tree that is controlled by, but is independent of Flutter's
2264 * element tree, i.e., widgets/elements/render objects. Flutter's semantics tree must be cached on
2265 * the Android side so that Android can query any {@code SemanticsNode} at any time. This class
2266 * represents a single node in the semantics tree, and it is a Java representation of the
2267 * analogous concept within Flutter.
2268 *
2269 * <p>To see how this {@code SemanticsNode}'s fields correspond to Flutter's semantics system, see
2270 * semantics.dart: https://github.com/flutter/engine/blob/main/lib/ui/semantics.dart
2271 */
2272 private static class SemanticsNode {
2273 private static boolean nullableHasAncestor(
2274 SemanticsNode target, Predicate<SemanticsNode> tester) {
2275 return target != null && target.getAncestor(tester) != null;
2276 }
2277
2278 final AccessibilityBridge accessibilityBridge;
2279
2280 // Flutter ID of this {@code SemanticsNode}.
2281 private int id = -1;
2282
2283 private int flags;
2284 private int actions;
2285 private int maxValueLength;
2286 private int currentValueLength;
2287 private int textSelectionBase;
2288 private int textSelectionExtent;
2289 private int platformViewId;
2290 private int scrollChildren;
2291 private int scrollIndex;
2292 private float scrollPosition;
2293 private float scrollExtentMax;
2294 private float scrollExtentMin;
2295 private String identifier;
2296 private String label;
2297 private List<StringAttribute> labelAttributes;
2298 private String value;
2299 private List<StringAttribute> valueAttributes;
2300 private String increasedValue;
2301 private List<StringAttribute> increasedValueAttributes;
2302 private String decreasedValue;
2303 private List<StringAttribute> decreasedValueAttributes;
2304 private String hint;
2305 private List<StringAttribute> hintAttributes;
2306
2307 // The textual description of the backing widget's tooltip.
2308 //
2309 // The tooltip is attached through AccessibilityNodeInfo.setTooltipText if
2310 // API level >= 28; otherwise, this is attached to the end of content description.
2311 @Nullable private String tooltip;
2312
2313 // The id of the sibling node that is before this node in traversal
2314 // order.
2315 //
2316 // The child order alone does not guarantee the TalkBack focus traversal
2317 // order. The AccessibilityNodeInfo.setTraversalAfter must be called with
2318 // its previous sibling to determine the focus traversal order.
2319 //
2320 // This property is updated in AccessibilityBridge.updateRecursively,
2321 // which is called at the end of every semantics update, and it is used in
2322 // AccessibilityBridge.createAccessibilityNodeInfo to set the "traversal
2323 // after" of this node.
2324 private int previousNodeId = -1;
2325
2326 // See Flutter's {@code SemanticsNode#textDirection}.
2327 private TextDirection textDirection;
2328
2329 private boolean hadPreviousConfig = false;
2330 private int previousFlags;
2331 private int previousActions;
2332 private int previousTextSelectionBase;
2333 private int previousTextSelectionExtent;
2334 private float previousScrollPosition;
2335 private float previousScrollExtentMax;
2336 private float previousScrollExtentMin;
2337 private String previousValue;
2338 private String previousLabel;
2339
2340 private float left;
2341 private float top;
2342 private float right;
2343 private float bottom;
2344 private float[] transform;
2345
2346 private SemanticsNode parent;
2347 private List<SemanticsNode> childrenInTraversalOrder = new ArrayList<>();
2348 private List<SemanticsNode> childrenInHitTestOrder = new ArrayList<>();
2349 private List<CustomAccessibilityAction> customAccessibilityActions;
2350 private CustomAccessibilityAction onTapOverride;
2351 private CustomAccessibilityAction onLongPressOverride;
2352
2353 private boolean inverseTransformDirty = true;
2354 private float[] inverseTransform;
2355
2356 private boolean globalGeometryDirty = true;
2357 private float[] globalTransform;
2358 private Rect globalRect;
2359
2360 SemanticsNode(@NonNull AccessibilityBridge accessibilityBridge) {
2361 this.accessibilityBridge = accessibilityBridge;
2362 }
2363
2364 /**
2365 * Returns the ancestor of this {@code SemanticsNode} for which {@link Predicate#test(Object)}
2366 * returns true, or null if no such ancestor exists.
2367 */
2368 private SemanticsNode getAncestor(Predicate<SemanticsNode> tester) {
2369 SemanticsNode nextAncestor = parent;
2370 while (nextAncestor != null) {
2371 if (tester.test(nextAncestor)) {
2372 return nextAncestor;
2373 }
2374 nextAncestor = nextAncestor.parent;
2375 }
2376 return null;
2377 }
2378
2379 /**
2380 * Returns true if the given {@code action} is supported by this {@code SemanticsNode}.
2381 *
2382 * <p>This method only applies to this {@code SemanticsNode} and does not implicitly search its
2383 * children.
2384 */
2385 private boolean hasAction(@NonNull Action action) {
2386 return (actions & action.value) != 0;
2387 }
2388
2389 /**
2390 * Returns true if the given {@code action} was supported by the immediately previous version of
2391 * this {@code SemanticsNode}.
2392 */
2393 private boolean hadAction(@NonNull Action action) {
2394 return (previousActions & action.value) != 0;
2395 }
2396
2397 private boolean hasFlag(@NonNull Flag flag) {
2398 return (flags & flag.value) != 0;
2399 }
2400
2401 private boolean hadFlag(@NonNull Flag flag) {
2402 if (BuildConfig.DEBUG && !hadPreviousConfig) {
2403 Log.e(TAG, "Attempted to check hadFlag but had no previous config.");
2404 }
2405 return (previousFlags & flag.value) != 0;
2406 }
2407
2408 private boolean didScroll() {
2409 return !Float.isNaN(scrollPosition)
2410 && !Float.isNaN(previousScrollPosition)
2411 && previousScrollPosition != scrollPosition;
2412 }
2413
2414 private boolean didChangeLabel() {
2415 if (label == null && previousLabel == null) {
2416 return false;
2417 }
2418 return label == null || previousLabel == null || !label.equals(previousLabel);
2419 }
2420
2421 private void log(@NonNull String indent, boolean recursive) {
2422 if (BuildConfig.DEBUG) {
2423 Log.i(
2424 TAG,
2425 indent
2426 + "SemanticsNode id="
2427 + id
2428 + " identifier="
2429 + identifier
2430 + " label="
2431 + label
2432 + " actions="
2433 + actions
2434 + " flags="
2435 + flags
2436 + "\n"
2437 + indent
2438 + " +-- textDirection="
2439 + textDirection
2440 + "\n"
2441 + indent
2442 + " +-- rect.ltrb=("
2443 + left
2444 + ", "
2445 + top
2446 + ", "
2447 + right
2448 + ", "
2449 + bottom
2450 + ")\n"
2451 + indent
2452 + " +-- transform="
2453 + Arrays.toString(transform)
2454 + "\n");
2455 if (recursive) {
2456 String childIndent = indent + " ";
2457 for (SemanticsNode child : childrenInTraversalOrder) {
2458 child.log(childIndent, recursive);
2459 }
2460 }
2461 }
2462 }
2463
2464 private void updateWith(
2465 @NonNull ByteBuffer buffer,
2466 @NonNull String[] strings,
2467 @NonNull ByteBuffer[] stringAttributeArgs) {
2468 hadPreviousConfig = true;
2469 previousValue = value;
2470 previousLabel = label;
2471 previousFlags = flags;
2472 previousActions = actions;
2473 previousTextSelectionBase = textSelectionBase;
2474 previousTextSelectionExtent = textSelectionExtent;
2475 previousScrollPosition = scrollPosition;
2476 previousScrollExtentMax = scrollExtentMax;
2477 previousScrollExtentMin = scrollExtentMin;
2478
2479 flags = buffer.getInt();
2480 actions = buffer.getInt();
2481 maxValueLength = buffer.getInt();
2482 currentValueLength = buffer.getInt();
2483 textSelectionBase = buffer.getInt();
2484 textSelectionExtent = buffer.getInt();
2485 platformViewId = buffer.getInt();
2486 scrollChildren = buffer.getInt();
2487 scrollIndex = buffer.getInt();
2488 scrollPosition = buffer.getFloat();
2489 scrollExtentMax = buffer.getFloat();
2490 scrollExtentMin = buffer.getFloat();
2491
2492 int stringIndex = buffer.getInt();
2493
2494 identifier = stringIndex == -1 ? null : strings[stringIndex];
2495 stringIndex = buffer.getInt();
2496
2497 label = stringIndex == -1 ? null : strings[stringIndex];
2498
2499 labelAttributes = getStringAttributesFromBuffer(buffer, stringAttributeArgs);
2500
2501 stringIndex = buffer.getInt();
2502 value = stringIndex == -1 ? null : strings[stringIndex];
2503
2504 valueAttributes = getStringAttributesFromBuffer(buffer, stringAttributeArgs);
2505
2506 stringIndex = buffer.getInt();
2507 increasedValue = stringIndex == -1 ? null : strings[stringIndex];
2508
2509 increasedValueAttributes = getStringAttributesFromBuffer(buffer, stringAttributeArgs);
2510
2511 stringIndex = buffer.getInt();
2512 decreasedValue = stringIndex == -1 ? null : strings[stringIndex];
2513
2514 decreasedValueAttributes = getStringAttributesFromBuffer(buffer, stringAttributeArgs);
2515
2516 stringIndex = buffer.getInt();
2517 hint = stringIndex == -1 ? null : strings[stringIndex];
2518
2519 hintAttributes = getStringAttributesFromBuffer(buffer, stringAttributeArgs);
2520
2521 stringIndex = buffer.getInt();
2522 tooltip = stringIndex == -1 ? null : strings[stringIndex];
2523
2524 textDirection = TextDirection.fromInt(buffer.getInt());
2525
2526 left = buffer.getFloat();
2527 top = buffer.getFloat();
2528 right = buffer.getFloat();
2529 bottom = buffer.getFloat();
2530
2531 if (transform == null) {
2532 transform = new float[16];
2533 }
2534 for (int i = 0; i < 16; ++i) {
2535 transform[i] = buffer.getFloat();
2536 }
2537 inverseTransformDirty = true;
2538 globalGeometryDirty = true;
2539
2540 final int childCount = buffer.getInt();
2541 childrenInTraversalOrder.clear();
2542 childrenInHitTestOrder.clear();
2543 for (int i = 0; i < childCount; ++i) {
2544 SemanticsNode child = accessibilityBridge.getOrCreateSemanticsNode(buffer.getInt());
2545 child.parent = this;
2546 childrenInTraversalOrder.add(child);
2547 }
2548 for (int i = 0; i < childCount; ++i) {
2549 SemanticsNode child = accessibilityBridge.getOrCreateSemanticsNode(buffer.getInt());
2550 child.parent = this;
2551 childrenInHitTestOrder.add(child);
2552 }
2553
2554 final int actionCount = buffer.getInt();
2555 if (actionCount == 0) {
2556 customAccessibilityActions = null;
2557 } else {
2558 if (customAccessibilityActions == null)
2559 customAccessibilityActions = new ArrayList<>(actionCount);
2560 else customAccessibilityActions.clear();
2561
2562 for (int i = 0; i < actionCount; i++) {
2563 CustomAccessibilityAction action =
2564 accessibilityBridge.getOrCreateAccessibilityAction(buffer.getInt());
2565 if (action.overrideId == Action.TAP.value) {
2566 onTapOverride = action;
2567 } else if (action.overrideId == Action.LONG_PRESS.value) {
2568 onLongPressOverride = action;
2569 } else {
2570 // If we receive a different overrideId it means that we were passed
2571 // a standard action to override that we don't yet support.
2572 if (BuildConfig.DEBUG && action.overrideId != -1) {
2573 Log.e(TAG, "Expected action.overrideId to be -1.");
2574 }
2575 customAccessibilityActions.add(action);
2576 }
2577 customAccessibilityActions.add(action);
2578 }
2579 }
2580 }
2581
2582 private List<StringAttribute> getStringAttributesFromBuffer(
2583 @NonNull ByteBuffer buffer, @NonNull ByteBuffer[] stringAttributeArgs) {
2584 final int attributesCount = buffer.getInt();
2585 if (attributesCount == -1) {
2586 return null;
2587 }
2588 final List<StringAttribute> result = new ArrayList<>(attributesCount);
2589 for (int i = 0; i < attributesCount; ++i) {
2590 final int start = buffer.getInt();
2591 final int end = buffer.getInt();
2592 final StringAttributeType type = StringAttributeType.values()[buffer.getInt()];
2593 switch (type) {
2594 case SPELLOUT:
2595 {
2596 // Pops the -1 size.
2597 buffer.getInt();
2598 SpellOutStringAttribute attribute = new SpellOutStringAttribute();
2599 attribute.start = start;
2600 attribute.end = end;
2601 attribute.type = type;
2602 result.add(attribute);
2603 break;
2604 }
2605 case LOCALE:
2606 {
2607 final int argsIndex = buffer.getInt();
2608 final ByteBuffer args = stringAttributeArgs[argsIndex];
2609 LocaleStringAttribute attribute = new LocaleStringAttribute();
2610 attribute.start = start;
2611 attribute.end = end;
2612 attribute.type = type;
2613 attribute.locale = Charset.forName("UTF-8").decode(args).toString();
2614 result.add(attribute);
2615 break;
2616 }
2617 default:
2618 break;
2619 }
2620 }
2621 return result;
2622 }
2623
2624 private void ensureInverseTransform() {
2625 if (!inverseTransformDirty) {
2626 return;
2627 }
2628 inverseTransformDirty = false;
2629 if (inverseTransform == null) {
2630 inverseTransform = new float[16];
2631 }
2632 if (!Matrix.invertM(inverseTransform, 0, transform, 0)) {
2633 Arrays.fill(inverseTransform, 0);
2634 }
2635 }
2636
2637 private Rect getGlobalRect() {
2638 if (BuildConfig.DEBUG && globalGeometryDirty) {
2639 Log.e(TAG, "Attempted to getGlobalRect with a dirty geometry.");
2640 }
2641 return globalRect;
2642 }
2643
2644 /**
2645 * Hit tests {@code point} to find the deepest focusable node in the node tree at that point.
2646 *
2647 * @param point The point to hit test against this node.
2648 * @param stopAtPlatformView Whether to return a platform view if found, regardless of whether
2649 * or not it is focusable.
2650 * @return The found node, or null if no relevant node was found at the given point.
2651 */
2652 private SemanticsNode hitTest(float[] point, boolean stopAtPlatformView) {
2653 final float w = point[3];
2654 final float x = point[0] / w;
2655 final float y = point[1] / w;
2656 if (x < left || x >= right || y < top || y >= bottom) return null;
2657 final float[] transformedPoint = new float[4];
2658 for (SemanticsNode child : childrenInHitTestOrder) {
2659 if (child.hasFlag(Flag.IS_HIDDEN)) {
2660 continue;
2661 }
2662 child.ensureInverseTransform();
2663 Matrix.multiplyMV(transformedPoint, 0, child.inverseTransform, 0, point, 0);
2664 final SemanticsNode result = child.hitTest(transformedPoint, stopAtPlatformView);
2665 if (result != null) {
2666 return result;
2667 }
2668 }
2669 final boolean foundPlatformView = stopAtPlatformView && platformViewId != -1;
2670 return isFocusable() || foundPlatformView ? this : null;
2671 }
2672
2673 // TODO(goderbauer): This should be decided by the framework once we have more information
2674 // about focusability there.
2675 private boolean isFocusable() {
2676 // We enforce in the framework that no other useful semantics are merged with these
2677 // nodes.
2678 if (hasFlag(Flag.SCOPES_ROUTE)) {
2679 return false;
2680 }
2681 if (hasFlag(Flag.IS_FOCUSABLE)) {
2682 return true;
2683 }
2684 // If not explicitly set as focusable, then use our legacy
2685 // algorithm. Once all focusable widgets have a Focus widget, then
2686 // this won't be needed.
2687 return (actions & ~SCROLLABLE_ACTIONS) != 0
2688 || (flags & FOCUSABLE_FLAGS) != 0
2689 || (label != null && !label.isEmpty())
2690 || (value != null && !value.isEmpty())
2691 || (hint != null && !hint.isEmpty());
2692 }
2693
2694 private void collectRoutes(List<SemanticsNode> edges) {
2695 if (hasFlag(Flag.SCOPES_ROUTE)) {
2696 edges.add(this);
2697 }
2698 for (SemanticsNode child : childrenInTraversalOrder) {
2699 child.collectRoutes(edges);
2700 }
2701 }
2702
2703 private String getRouteName() {
2704 // Returns the first non-null and non-empty semantic label of a child
2705 // with an NamesRoute flag. Otherwise returns null.
2706 if (hasFlag(Flag.NAMES_ROUTE)) {
2707 if (label != null && !label.isEmpty()) {
2708 return label;
2709 }
2710 }
2711 for (SemanticsNode child : childrenInTraversalOrder) {
2712 String newName = child.getRouteName();
2713 if (newName != null && !newName.isEmpty()) {
2714 return newName;
2715 }
2716 }
2717 return null;
2718 }
2719
2720 private void updateRecursively(
2721 float[] ancestorTransform, Set<SemanticsNode> visitedObjects, boolean forceUpdate) {
2722 visitedObjects.add(this);
2723
2724 if (globalGeometryDirty) {
2725 forceUpdate = true;
2726 }
2727
2728 if (forceUpdate) {
2729 if (globalTransform == null) {
2730 globalTransform = new float[16];
2731 }
2732 if (transform == null) {
2733 if (BuildConfig.DEBUG) {
2734 Log.e(TAG, "transform has not been initialized for id = " + id);
2735 accessibilityBridge.getRootSemanticsNode().log("Semantics tree:", true);
2736 }
2737 transform = new float[16];
2738 }
2739 Matrix.multiplyMM(globalTransform, 0, ancestorTransform, 0, transform, 0);
2740
2741 final float[] sample = new float[4];
2742 sample[2] = 0;
2743 sample[3] = 1;
2744
2745 final float[] point1 = new float[4];
2746 final float[] point2 = new float[4];
2747 final float[] point3 = new float[4];
2748 final float[] point4 = new float[4];
2749
2750 sample[0] = left;
2751 sample[1] = top;
2752 transformPoint(point1, globalTransform, sample);
2753
2754 sample[0] = right;
2755 sample[1] = top;
2756 transformPoint(point2, globalTransform, sample);
2757
2758 sample[0] = right;
2759 sample[1] = bottom;
2760 transformPoint(point3, globalTransform, sample);
2761
2762 sample[0] = left;
2763 sample[1] = bottom;
2764 transformPoint(point4, globalTransform, sample);
2765
2766 if (globalRect == null) globalRect = new Rect();
2767
2768 globalRect.set(
2769 Math.round(min(point1[0], point2[0], point3[0], point4[0])),
2770 Math.round(min(point1[1], point2[1], point3[1], point4[1])),
2771 Math.round(max(point1[0], point2[0], point3[0], point4[0])),
2772 Math.round(max(point1[1], point2[1], point3[1], point4[1])));
2773
2774 globalGeometryDirty = false;
2775 }
2776
2777 if (BuildConfig.DEBUG) {
2778 if (globalTransform == null) {
2779 Log.e(TAG, "Expected globalTransform to not be null.");
2780 }
2781 if (globalRect == null) {
2782 Log.e(TAG, "Expected globalRect to not be null.");
2783 }
2784 }
2785
2786 int previousNodeId = -1;
2787 for (SemanticsNode child : childrenInTraversalOrder) {
2788 child.previousNodeId = previousNodeId;
2789 previousNodeId = child.id;
2790 child.updateRecursively(globalTransform, visitedObjects, forceUpdate);
2791 }
2792 }
2793
2794 private void transformPoint(float[] result, float[] transform, float[] point) {
2795 Matrix.multiplyMV(result, 0, transform, 0, point, 0);
2796 final float w = result[3];
2797 result[0] /= w;
2798 result[1] /= w;
2799 result[2] /= w;
2800 result[3] = 0;
2801 }
2802
2803 private float min(float a, float b, float c, float d) {
2804 return Math.min(a, Math.min(b, Math.min(c, d)));
2805 }
2806
2807 private float max(float a, float b, float c, float d) {
2808 return Math.max(a, Math.max(b, Math.max(c, d)));
2809 }
2810
2811 private CharSequence getValue() {
2812 return createSpannableString(value, valueAttributes);
2813 }
2814
2815 private CharSequence getLabel() {
2816 return createSpannableString(label, labelAttributes);
2817 }
2818
2819 private CharSequence getHint() {
2820 return createSpannableString(hint, hintAttributes);
2821 }
2822
2823 private CharSequence getValueLabelHint() {
2824 CharSequence[] array = new CharSequence[] {getValue(), getLabel(), getHint()};
2825 CharSequence result = null;
2826 for (CharSequence word : array) {
2827 if (word != null && word.length() > 0) {
2828 if (result == null || result.length() == 0) {
2829 result = word;
2830 } else {
2831 result = TextUtils.concat(result, ", ", word);
2832 }
2833 }
2834 }
2835 return result;
2836 }
2837
2838 private CharSequence getTextFieldHint() {
2839 CharSequence[] array = new CharSequence[] {getLabel(), getHint()};
2840 CharSequence result = null;
2841 for (CharSequence word : array) {
2842 if (word != null && word.length() > 0) {
2843 if (result == null || result.length() == 0) {
2844 result = word;
2845 } else {
2846 result = TextUtils.concat(result, ", ", word);
2847 }
2848 }
2849 }
2850 return result;
2851 }
2852
2853 private SpannableString createSpannableString(String string, List<StringAttribute> attributes) {
2854 if (string == null) {
2855 return null;
2856 }
2857 final SpannableString spannableString = new SpannableString(string);
2858 if (attributes != null) {
2859 for (StringAttribute attribute : attributes) {
2860 switch (attribute.type) {
2861 case SPELLOUT:
2862 {
2863 final TtsSpan ttsSpan = new TtsSpan.Builder<>(TtsSpan.TYPE_VERBATIM).build();
2864 spannableString.setSpan(ttsSpan, attribute.start, attribute.end, 0);
2865 break;
2866 }
2867 case LOCALE:
2868 {
2869 LocaleStringAttribute localeAttribute = (LocaleStringAttribute) attribute;
2870 Locale locale = Locale.forLanguageTag(localeAttribute.locale);
2871 final LocaleSpan localeSpan = new LocaleSpan(locale);
2872 spannableString.setSpan(localeSpan, attribute.start, attribute.end, 0);
2873 break;
2874 }
2875 }
2876 }
2877 }
2878 return spannableString;
2879 }
2880 }
2881
2882 /**
2883 * Delegates handling of {@link android.view.ViewParent#requestSendAccessibilityEvent} to the
2884 * accessibility bridge.
2885 *
2886 * <p>This is used by embedded platform views to propagate accessibility events from their view
2887 * hierarchy to the accessibility bridge.
2888 *
2889 * <p>As the embedded view doesn't have to be the only View in the embedded hierarchy (it can have
2890 * child views) and the event might have been originated from any view in this hierarchy, this
2891 * method gets both a reference to the embedded platform view, and a reference to the view from
2892 * its hierarchy that sent the event.
2893 *
2894 * @param embeddedView the embedded platform view for which the event is delegated
2895 * @param eventOrigin the view in the embedded view's hierarchy that sent the event.
2896 * @return True if the event was sent.
2897 */
2898 // AccessibilityEvent has many irrelevant cases that would be confusing to list.
2899 @SuppressLint("SwitchIntDef")
2900 public boolean externalViewRequestSendAccessibilityEvent(
2901 View embeddedView, View eventOrigin, AccessibilityEvent event) {
2902 if (!accessibilityViewEmbedder.requestSendAccessibilityEvent(
2903 embeddedView, eventOrigin, event)) {
2904 return false;
2905 }
2906 Integer virtualNodeId = accessibilityViewEmbedder.getRecordFlutterId(embeddedView, event);
2907 if (virtualNodeId == null) {
2908 return false;
2909 }
2910 switch (event.getEventType()) {
2911 case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
2912 hoveredObject = null;
2913 break;
2914 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
2915 embeddedAccessibilityFocusedNodeId = virtualNodeId;
2916 accessibilityFocusedSemanticsNode = null;
2917 break;
2918 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
2919 embeddedInputFocusedNodeId = null;
2920 embeddedAccessibilityFocusedNodeId = null;
2921 break;
2922 case AccessibilityEvent.TYPE_VIEW_FOCUSED:
2923 embeddedInputFocusedNodeId = virtualNodeId;
2924 inputFocusedSemanticsNode = null;
2925 break;
2926 }
2927 return true;
2928 }
2929}
m reset()
GLenum type
void add(sk_sp< SkIDChangeListener > listener) SK_EXCLUDES(fMutex)
static final int API_31
Definition: Build.java:21
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
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::@2836::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: TestRunner.cpp:137
StringAttribute
Definition: ax_enums.h:521
def Build(configs, env, options)
Definition: build.py:232
intptr_t word
Definition: globals.h:500
def build()
Definition: dom.py:52
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 buffer
Definition: switches.h:126
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:769
SK_API sk_sp< PrecompileColorFilter > Matrix()
Definition: tester.py:1
static SkColor4f transform(SkColor4f c, SkColorSpace *src, SkColorSpace *dst)
Definition: p3.cpp:47
SkScalar w
const uintptr_t id