public AccessibilityNodeInfo createAccessibilityNodeInfo()

in shell/platform/android/io/flutter/view/AccessibilityBridge.java [553:920]


  public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
    if (virtualViewId >= MIN_ENGINE_GENERATED_NODE_ID) {
      // The node is in the engine generated range, and is provided by the accessibility view
      // embedder.
      return accessibilityViewEmbedder.createAccessibilityNodeInfo(virtualViewId);
    }

    if (virtualViewId == View.NO_ID) {
      AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(rootAccessibilityView);
      rootAccessibilityView.onInitializeAccessibilityNodeInfo(result);
      // TODO(mattcarroll): what does it mean for the semantics tree to contain or not contain
      //                    the root node ID?
      if (flutterSemanticsTree.containsKey(ROOT_NODE_ID)) {
        result.addChild(rootAccessibilityView, ROOT_NODE_ID);
      }
      return result;
    }

    SemanticsNode semanticsNode = flutterSemanticsTree.get(virtualViewId);
    if (semanticsNode == null) {
      return null;
    }

    // Generate accessibility node for platform views using a virtual display.
    //
    // In this case, register the accessibility node in the view embedder,
    // so the accessibility tree can be mirrored as a subtree of the Flutter accessibility tree.
    // This is in constrast to hybrid composition where the embedded view is in the view hiearchy,
    // so it doesn't need to be mirrored.
    //
    // See the case down below for how hybrid composition is handled.
    if (semanticsNode.platformViewId != -1) {
      View embeddedView =
          platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
      if (platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
        Rect bounds = semanticsNode.getGlobalRect();
        return accessibilityViewEmbedder.getRootNode(embeddedView, semanticsNode.id, bounds);
      }
    }

    AccessibilityNodeInfo result =
        obtainAccessibilityNodeInfo(rootAccessibilityView, virtualViewId);
    // Work around for https://github.com/flutter/flutter/issues/2101
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
      result.setViewIdResourceName("");
    }
    result.setPackageName(rootAccessibilityView.getContext().getPackageName());
    result.setClassName("android.view.View");
    result.setSource(rootAccessibilityView, virtualViewId);
    result.setFocusable(semanticsNode.isFocusable());
    if (inputFocusedSemanticsNode != null) {
      result.setFocused(inputFocusedSemanticsNode.id == virtualViewId);
    }

    if (accessibilityFocusedSemanticsNode != null) {
      result.setAccessibilityFocused(accessibilityFocusedSemanticsNode.id == virtualViewId);
    }

    if (semanticsNode.hasFlag(Flag.IS_TEXT_FIELD)) {
      result.setPassword(semanticsNode.hasFlag(Flag.IS_OBSCURED));
      if (!semanticsNode.hasFlag(Flag.IS_READ_ONLY)) {
        result.setClassName("android.widget.EditText");
      }
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
        result.setEditable(!semanticsNode.hasFlag(Flag.IS_READ_ONLY));
        if (semanticsNode.textSelectionBase != -1 && semanticsNode.textSelectionExtent != -1) {
          result.setTextSelection(
              semanticsNode.textSelectionBase, semanticsNode.textSelectionExtent);
        }
        // Text fields will always be created as a live region when they have input focus,
        // so that updates to the label trigger polite announcements. This makes it easy to
        // follow a11y guidelines for text fields on Android.
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2
            && accessibilityFocusedSemanticsNode != null
            && accessibilityFocusedSemanticsNode.id == virtualViewId) {
          result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
        }
      }

      // Cursor movements
      int granularities = 0;
      if (semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) {
        result.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
        granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER;
      }
      if (semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) {
        result.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
        granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER;
      }
      if (semanticsNode.hasAction(Action.MOVE_CURSOR_FORWARD_BY_WORD)) {
        result.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
        granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD;
      }
      if (semanticsNode.hasAction(Action.MOVE_CURSOR_BACKWARD_BY_WORD)) {
        result.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
        granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD;
      }
      result.setMovementGranularities(granularities);
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
          && semanticsNode.maxValueLength >= 0) {
        // Account for the fact that Flutter is counting Unicode scalar values and Android
        // is counting UTF16 words.
        final int length = semanticsNode.value == null ? 0 : semanticsNode.value.length();
        int a = length - semanticsNode.currentValueLength + semanticsNode.maxValueLength;
        result.setMaxTextLength(
            length - semanticsNode.currentValueLength + semanticsNode.maxValueLength);
      }
    }

    // These are non-ops on older devices. Attempting to interact with the text will cause Talkback
    // to read the contents of the text box instead.
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
      if (semanticsNode.hasAction(Action.SET_SELECTION)) {
        result.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
      }
      if (semanticsNode.hasAction(Action.COPY)) {
        result.addAction(AccessibilityNodeInfo.ACTION_COPY);
      }
      if (semanticsNode.hasAction(Action.CUT)) {
        result.addAction(AccessibilityNodeInfo.ACTION_CUT);
      }
      if (semanticsNode.hasAction(Action.PASTE)) {
        result.addAction(AccessibilityNodeInfo.ACTION_PASTE);
      }
    }

    // Set text API isn't available until API 21.
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      if (semanticsNode.hasAction(Action.SET_TEXT)) {
        result.addAction(AccessibilityNodeInfo.ACTION_SET_TEXT);
      }
    }

    if (semanticsNode.hasFlag(Flag.IS_BUTTON) || semanticsNode.hasFlag(Flag.IS_LINK)) {
      result.setClassName("android.widget.Button");
    }
    if (semanticsNode.hasFlag(Flag.IS_IMAGE)) {
      result.setClassName("android.widget.ImageView");
      // TODO(jonahwilliams): Figure out a way conform to the expected id from TalkBack's
      // CustomLabelManager. talkback/src/main/java/labeling/CustomLabelManager.java#L525
    }
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2
        && semanticsNode.hasAction(Action.DISMISS)) {
      result.setDismissable(true);
      result.addAction(AccessibilityNodeInfo.ACTION_DISMISS);
    }

    if (semanticsNode.parent != null) {
      if (BuildConfig.DEBUG && semanticsNode.id <= ROOT_NODE_ID) {
        Log.e(TAG, "Semantics node id is not > ROOT_NODE_ID.");
      }
      result.setParent(rootAccessibilityView, semanticsNode.parent.id);
    } else {
      if (BuildConfig.DEBUG && semanticsNode.id != ROOT_NODE_ID) {
        Log.e(TAG, "Semantics node id does not equal ROOT_NODE_ID.");
      }
      result.setParent(rootAccessibilityView);
    }

    if (semanticsNode.previousNodeId != -1
        && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
      result.setTraversalAfter(rootAccessibilityView, semanticsNode.previousNodeId);
    }

    Rect bounds = semanticsNode.getGlobalRect();
    if (semanticsNode.parent != null) {
      Rect parentBounds = semanticsNode.parent.getGlobalRect();
      Rect boundsInParent = new Rect(bounds);
      boundsInParent.offset(-parentBounds.left, -parentBounds.top);
      result.setBoundsInParent(boundsInParent);
    } else {
      result.setBoundsInParent(bounds);
    }
    final Rect boundsInScreen = getBoundsInScreen(bounds);
    result.setBoundsInScreen(boundsInScreen);
    result.setVisibleToUser(true);
    result.setEnabled(
        !semanticsNode.hasFlag(Flag.HAS_ENABLED_STATE) || semanticsNode.hasFlag(Flag.IS_ENABLED));

    if (semanticsNode.hasAction(Action.TAP)) {
      if (Build.VERSION.SDK_INT >= 21 && semanticsNode.onTapOverride != null) {
        result.addAction(
            new AccessibilityNodeInfo.AccessibilityAction(
                AccessibilityNodeInfo.ACTION_CLICK, semanticsNode.onTapOverride.hint));
        result.setClickable(true);
      } else {
        result.addAction(AccessibilityNodeInfo.ACTION_CLICK);
        result.setClickable(true);
      }
    }
    if (semanticsNode.hasAction(Action.LONG_PRESS)) {
      if (Build.VERSION.SDK_INT >= 21 && semanticsNode.onLongPressOverride != null) {
        result.addAction(
            new AccessibilityNodeInfo.AccessibilityAction(
                AccessibilityNodeInfo.ACTION_LONG_CLICK, semanticsNode.onLongPressOverride.hint));
        result.setLongClickable(true);
      } else {
        result.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
        result.setLongClickable(true);
      }
    }
    if (semanticsNode.hasAction(Action.SCROLL_LEFT)
        || semanticsNode.hasAction(Action.SCROLL_UP)
        || semanticsNode.hasAction(Action.SCROLL_RIGHT)
        || semanticsNode.hasAction(Action.SCROLL_DOWN)) {
      result.setScrollable(true);

      // This tells Android's a11y to send scroll events when reaching the end of
      // the visible viewport of a scrollable, unless the node itself does not
      // allow implicit scrolling - then we leave the className as view.View.
      //
      // We should prefer setCollectionInfo to the class names, as this way we get "In List"
      // and "Out of list" announcements.  But we don't always know the counts, so we
      // can fallback to the generic scroll view class names.
      //
      // On older APIs, we always fall back to the generic scroll view class names here.
      //
      // TODO(dnfield): We should add semantics properties for rows and columns in 2 dimensional
      // lists, e.g.
      // GridView.  Right now, we're only supporting ListViews and only if they have scroll
      // children.
      if (semanticsNode.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)) {
        if (semanticsNode.hasAction(Action.SCROLL_LEFT)
            || semanticsNode.hasAction(Action.SCROLL_RIGHT)) {
          if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT
              && shouldSetCollectionInfo(semanticsNode)) {
            result.setCollectionInfo(
                AccessibilityNodeInfo.CollectionInfo.obtain(
                    0, // rows
                    semanticsNode.scrollChildren, // columns
                    false // hierarchical
                    ));
          } else {
            result.setClassName("android.widget.HorizontalScrollView");
          }
        } else {
          if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2
              && shouldSetCollectionInfo(semanticsNode)) {
            result.setCollectionInfo(
                AccessibilityNodeInfo.CollectionInfo.obtain(
                    semanticsNode.scrollChildren, // rows
                    0, // columns
                    false // hierarchical
                    ));
          } else {
            result.setClassName("android.widget.ScrollView");
          }
        }
      }
      // TODO(ianh): Once we're on SDK v23+, call addAction to
      // expose AccessibilityAction.ACTION_SCROLL_LEFT, _RIGHT,
      // _UP, and _DOWN when appropriate.
      if (semanticsNode.hasAction(Action.SCROLL_LEFT)
          || semanticsNode.hasAction(Action.SCROLL_UP)) {
        result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
      }
      if (semanticsNode.hasAction(Action.SCROLL_RIGHT)
          || semanticsNode.hasAction(Action.SCROLL_DOWN)) {
        result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
      }
    }
    if (semanticsNode.hasAction(Action.INCREASE) || semanticsNode.hasAction(Action.DECREASE)) {
      // TODO(jonahwilliams): support AccessibilityAction.ACTION_SET_PROGRESS once SDK is
      // updated.
      result.setClassName("android.widget.SeekBar");
      if (semanticsNode.hasAction(Action.INCREASE)) {
        result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
      }
      if (semanticsNode.hasAction(Action.DECREASE)) {
        result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
      }
    }
    if (semanticsNode.hasFlag(Flag.IS_LIVE_REGION)
        && Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
      result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
    }

    // Scopes routes are not focusable, only need to set the content
    // for non-scopes-routes semantics nodes.
    if (semanticsNode.hasFlag(Flag.IS_TEXT_FIELD)) {
      result.setText(semanticsNode.getValueLabelHint());
    } else if (!semanticsNode.hasFlag(Flag.SCOPES_ROUTE)) {
      CharSequence content = semanticsNode.getValueLabelHint();
      if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
        if (semanticsNode.tooltip != null) {
          // For backward compatibility with Flutter SDK before Android API
          // level 28, the tooltip is appended at the end of content description.
          content = content != null ? content : "";
          content = content + "\n" + semanticsNode.tooltip;
        }
      }
      if (content != null) {
        result.setContentDescription(content);
      }
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
      if (semanticsNode.tooltip != null) {
        result.setTooltipText(semanticsNode.tooltip);
      }
    }

    boolean hasCheckedState = semanticsNode.hasFlag(Flag.HAS_CHECKED_STATE);
    boolean hasToggledState = semanticsNode.hasFlag(Flag.HAS_TOGGLED_STATE);
    if (BuildConfig.DEBUG && (hasCheckedState && hasToggledState)) {
      Log.e(TAG, "Expected semanticsNode to have checked state and toggled state.");
    }
    result.setCheckable(hasCheckedState || hasToggledState);
    if (hasCheckedState) {
      result.setChecked(semanticsNode.hasFlag(Flag.IS_CHECKED));
      if (semanticsNode.hasFlag(Flag.IS_IN_MUTUALLY_EXCLUSIVE_GROUP)) {
        result.setClassName("android.widget.RadioButton");
      } else {
        result.setClassName("android.widget.CheckBox");
      }
    } else if (hasToggledState) {
      result.setChecked(semanticsNode.hasFlag(Flag.IS_TOGGLED));
      result.setClassName("android.widget.Switch");
    }
    result.setSelected(semanticsNode.hasFlag(Flag.IS_SELECTED));

    // Heading support
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
      result.setHeading(semanticsNode.hasFlag(Flag.IS_HEADER));
    }

    // Accessibility Focus
    if (accessibilityFocusedSemanticsNode != null
        && accessibilityFocusedSemanticsNode.id == virtualViewId) {
      result.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
    } else {
      result.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
    }

    // Actions on the local context menu
    if (Build.VERSION.SDK_INT >= 21) {
      if (semanticsNode.customAccessibilityActions != null) {
        for (CustomAccessibilityAction action : semanticsNode.customAccessibilityActions) {
          result.addAction(
              new AccessibilityNodeInfo.AccessibilityAction(action.resourceId, action.label));
        }
      }
    }

    for (SemanticsNode child : semanticsNode.childrenInTraversalOrder) {
      if (child.hasFlag(Flag.IS_HIDDEN)) {
        continue;
      }
      if (child.platformViewId != -1) {
        View embeddedView =
            platformViewsAccessibilityDelegate.getPlatformViewById(child.platformViewId);

        // Add the embedded view as a child of the current accessibility node if it's using
        // hybrid composition.
        //
        // In this case, the view is in the Activity's view hierarchy, so it doesn't need to be
        // mirrored.
        //
        // See the case above for how virtual displays are handled.
        if (!platformViewsAccessibilityDelegate.usesVirtualDisplay(child.platformViewId)) {
          result.addChild(embeddedView);
          continue;
        }
      }
      result.addChild(rootAccessibilityView, child.id);
    }
    return result;
  }