void updateSemantics()

in shell/platform/android/io/flutter/view/AccessibilityBridge.java [1530:1764]


  void updateSemantics(
      @NonNull ByteBuffer buffer,
      @NonNull String[] strings,
      @NonNull ByteBuffer[] stringAttributeArgs) {
    ArrayList<SemanticsNode> updated = new ArrayList<>();
    while (buffer.hasRemaining()) {
      int id = buffer.getInt();
      SemanticsNode semanticsNode = getOrCreateSemanticsNode(id);
      semanticsNode.updateWith(buffer, strings, stringAttributeArgs);
      if (semanticsNode.hasFlag(Flag.IS_HIDDEN)) {
        continue;
      }
      if (semanticsNode.hasFlag(Flag.IS_FOCUSED)) {
        inputFocusedSemanticsNode = semanticsNode;
      }
      if (semanticsNode.hadPreviousConfig) {
        updated.add(semanticsNode);
      }
      if (semanticsNode.platformViewId != -1
          && !platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
        View embeddedView =
            platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
        if (embeddedView != null) {
          embeddedView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
        }
      }
    }

    Set<SemanticsNode> visitedObjects = new HashSet<>();
    SemanticsNode rootObject = getRootSemanticsNode();
    List<SemanticsNode> newRoutes = new ArrayList<>();
    if (rootObject != null) {
      final float[] identity = new float[16];
      Matrix.setIdentityM(identity, 0);
      // In Android devices API 23 and above, the system nav bar can be placed on the left side
      // of the screen in landscape mode. We must handle the translation ourselves for the
      // a11y nodes.
      if (Build.VERSION.SDK_INT >= 23) {
        boolean needsToApplyLeftCutoutInset = true;
        // In Android devices API 28 and above, the `layoutInDisplayCutoutMode` window attribute
        // can be set to allow overlapping content within the cutout area. Query the attribute
        // to figure out whether the content overlaps with the cutout and decide whether to
        // apply cutout inset.
        if (Build.VERSION.SDK_INT >= 28) {
          needsToApplyLeftCutoutInset = doesLayoutInDisplayCutoutModeRequireLeftInset();
        }

        if (needsToApplyLeftCutoutInset) {
          WindowInsets insets = rootAccessibilityView.getRootWindowInsets();
          if (insets != null) {
            if (!lastLeftFrameInset.equals(insets.getSystemWindowInsetLeft())) {
              rootObject.globalGeometryDirty = true;
              rootObject.inverseTransformDirty = true;
            }
            lastLeftFrameInset = insets.getSystemWindowInsetLeft();
            Matrix.translateM(identity, 0, lastLeftFrameInset, 0, 0);
          }
        }
      }
      rootObject.updateRecursively(identity, visitedObjects, false);
      rootObject.collectRoutes(newRoutes);
    }

    // Dispatch a TYPE_WINDOW_STATE_CHANGED event if the most recent route id changed from the
    // previously cached route id.

    // Finds the last route that is not in the previous routes.
    SemanticsNode lastAdded = null;
    for (SemanticsNode semanticsNode : newRoutes) {
      if (!flutterNavigationStack.contains(semanticsNode.id)) {
        lastAdded = semanticsNode;
      }
    }

    // If all the routes are in the previous route, get the last route.
    if (lastAdded == null && newRoutes.size() > 0) {
      lastAdded = newRoutes.get(newRoutes.size() - 1);
    }

    // There are two cases if lastAdded != nil
    // 1. lastAdded is not in previous routes. In this case,
    //    lastAdded.id != previousRouteId
    // 2. All new routes are in previous routes and
    //    lastAdded = newRoutes.last.
    // In the first case, we need to announce new route. In the second case,
    // we need to announce if one list is shorter than the other.
    if (lastAdded != null
        && (lastAdded.id != previousRouteId || newRoutes.size() != flutterNavigationStack.size())) {
      previousRouteId = lastAdded.id;
      onWindowNameChange(lastAdded);
    }
    flutterNavigationStack.clear();
    for (SemanticsNode semanticsNode : newRoutes) {
      flutterNavigationStack.add(semanticsNode.id);
    }

    Iterator<Map.Entry<Integer, SemanticsNode>> it = flutterSemanticsTree.entrySet().iterator();
    while (it.hasNext()) {
      Map.Entry<Integer, SemanticsNode> entry = it.next();
      SemanticsNode object = entry.getValue();
      if (!visitedObjects.contains(object)) {
        willRemoveSemanticsNode(object);
        it.remove();
      }
    }

    // TODO(goderbauer): Send this event only once (!) for changed subtrees,
    //     see https://github.com/flutter/flutter/issues/14534
    sendWindowContentChangeEvent(0);

    for (SemanticsNode object : updated) {
      if (object.didScroll()) {
        AccessibilityEvent event =
            obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_SCROLLED);

        // Android doesn't support unbound scrolling. So we pretend there is a large
        // bound (SCROLL_EXTENT_FOR_INFINITY), which you can never reach.
        float position = object.scrollPosition;
        float max = object.scrollExtentMax;
        if (Float.isInfinite(object.scrollExtentMax)) {
          max = SCROLL_EXTENT_FOR_INFINITY;
          if (position > SCROLL_POSITION_CAP_FOR_INFINITY) {
            position = SCROLL_POSITION_CAP_FOR_INFINITY;
          }
        }
        if (Float.isInfinite(object.scrollExtentMin)) {
          max += SCROLL_EXTENT_FOR_INFINITY;
          if (position < -SCROLL_POSITION_CAP_FOR_INFINITY) {
            position = -SCROLL_POSITION_CAP_FOR_INFINITY;
          }
          position += SCROLL_EXTENT_FOR_INFINITY;
        } else {
          max -= object.scrollExtentMin;
          position -= object.scrollExtentMin;
        }

        if (object.hadAction(Action.SCROLL_UP) || object.hadAction(Action.SCROLL_DOWN)) {
          event.setScrollY((int) position);
          event.setMaxScrollY((int) max);
        } else if (object.hadAction(Action.SCROLL_LEFT) || object.hadAction(Action.SCROLL_RIGHT)) {
          event.setScrollX((int) position);
          event.setMaxScrollX((int) max);
        }
        if (object.scrollChildren > 0) {
          // We don't need to add 1 to the scroll index because TalkBack does this automagically.
          event.setItemCount(object.scrollChildren);
          event.setFromIndex(object.scrollIndex);
          int visibleChildren = 0;
          // handle hidden children at the beginning and end of the list.
          for (SemanticsNode child : object.childrenInHitTestOrder) {
            if (!child.hasFlag(Flag.IS_HIDDEN)) {
              visibleChildren += 1;
            }
          }
          if (BuildConfig.DEBUG) {
            if (object.scrollIndex + visibleChildren > object.scrollChildren) {
              Log.e(TAG, "Scroll index is out of bounds.");
            }

            if (object.childrenInHitTestOrder.isEmpty()) {
              Log.e(TAG, "Had scrollChildren but no childrenInHitTestOrder");
            }
          }
          // The setToIndex should be the index of the last visible child. Because we counted all
          // children, including the first index we need to subtract one.
          //
          //   [0, 1, 2, 3, 4, 5]
          //    ^     ^
          // In the example above where 0 is the first visible index and 2 is the last, we will
          // count 3 total visible children. We then subtract one to get the correct last visible
          // index of 2.
          event.setToIndex(object.scrollIndex + visibleChildren - 1);
        }
        sendAccessibilityEvent(event);
      }
      if (object.hasFlag(Flag.IS_LIVE_REGION) && object.didChangeLabel()) {
        sendWindowContentChangeEvent(object.id);
      }
      if (accessibilityFocusedSemanticsNode != null
          && accessibilityFocusedSemanticsNode.id == object.id
          && !object.hadFlag(Flag.IS_SELECTED)
          && object.hasFlag(Flag.IS_SELECTED)) {
        AccessibilityEvent event =
            obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_SELECTED);
        event.getText().add(object.label);
        sendAccessibilityEvent(event);
      }

      // If the object is the input-focused node, then tell the reader about it, but only if
      // it has changed since the last update.
      if (inputFocusedSemanticsNode != null
          && inputFocusedSemanticsNode.id == object.id
          && (lastInputFocusedSemanticsNode == null
              || lastInputFocusedSemanticsNode.id != inputFocusedSemanticsNode.id)) {
        lastInputFocusedSemanticsNode = inputFocusedSemanticsNode;
        sendAccessibilityEvent(
            obtainAccessibilityEvent(object.id, AccessibilityEvent.TYPE_VIEW_FOCUSED));
      } else if (inputFocusedSemanticsNode == null) {
        // There's no TYPE_VIEW_CLEAR_FOCUSED event, so if the current input focus becomes
        // null, then we just set the last one to null too, so that it sends the event again
        // when something regains focus.
        lastInputFocusedSemanticsNode = null;
      }

      if (inputFocusedSemanticsNode != null
          && inputFocusedSemanticsNode.id == object.id
          && object.hadFlag(Flag.IS_TEXT_FIELD)
          && object.hasFlag(Flag.IS_TEXT_FIELD)
          // If we have a TextField that has InputFocus, we should avoid announcing it if something
          // else we track has a11y focus. This needs to still work when, e.g., IME has a11y focus
          // or the "PASTE" popup is used though.
          // See more discussion at https://github.com/flutter/flutter/issues/23180
          && (accessibilityFocusedSemanticsNode == null
              || (accessibilityFocusedSemanticsNode.id == inputFocusedSemanticsNode.id))) {
        String oldValue = object.previousValue != null ? object.previousValue : "";
        String newValue = object.value != null ? object.value : "";
        AccessibilityEvent event = createTextChangedEvent(object.id, oldValue, newValue);
        if (event != null) {
          sendAccessibilityEvent(event);
        }

        if (object.previousTextSelectionBase != object.textSelectionBase
            || object.previousTextSelectionExtent != object.textSelectionExtent) {
          AccessibilityEvent selectionEvent =
              obtainAccessibilityEvent(
                  object.id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
          selectionEvent.getText().add(newValue);
          selectionEvent.setFromIndex(object.textSelectionBase);
          selectionEvent.setToIndex(object.textSelectionExtent);
          selectionEvent.setItemCount(newValue.length());
          sendAccessibilityEvent(selectionEvent);
        }
      }
    }
  }