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