81 private static final String TAG =
"AccessibilityBridge";
86 private static final int ACTION_SHOW_ON_SCREEN = 16908342;
88 private static final float SCROLL_EXTENT_FOR_INFINITY = 100000.0f;
89 private static final float SCROLL_POSITION_CAP_FOR_INFINITY = 70000.0f;
90 private static final int ROOT_NODE_ID = 0;
91 private static final int SCROLLABLE_ACTIONS =
92 Action.SCROLL_RIGHT.value
93 | Action.SCROLL_LEFT.value
94 | Action.SCROLL_UP.value
95 | Action.SCROLL_DOWN.value;
97 private static final int FOCUSABLE_FLAGS =
98 Flag.HAS_CHECKED_STATE.value
99 | Flag.IS_CHECKED.value
100 | Flag.IS_SELECTED.value
101 | Flag.IS_TEXT_FIELD.value
102 | Flag.IS_FOCUSED.value
103 | Flag.HAS_ENABLED_STATE.value
104 | Flag.IS_ENABLED.value
105 | Flag.IS_IN_MUTUALLY_EXCLUSIVE_GROUP.value
106 | Flag.HAS_TOGGLED_STATE.value
107 | Flag.IS_TOGGLED.value
108 | Flag.IS_FOCUSABLE.value
109 | Flag.IS_SLIDER.value;
124 private static final int MIN_ENGINE_GENERATED_NODE_ID = 1 << 16;
127 private static final int BOLD_TEXT_WEIGHT_ADJUSTMENT = 300;
130 private static int FIRST_RESOURCE_ID = 267386881;
133 @NonNull
private final View rootAccessibilityView;
137 @NonNull
private final AccessibilityChannel accessibilityChannel;
141 @NonNull
private final AccessibilityManager accessibilityManager;
147 @NonNull
private final PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate;
153 @NonNull
private final ContentResolver contentResolver;
167 @NonNull
private final Map<Integer, SemanticsNode> flutterSemanticsTree =
new HashMap<>();
194 private final Map<Integer, CustomAccessibilityAction> customAccessibilityActions =
201 @Nullable
private SemanticsNode accessibilityFocusedSemanticsNode;
208 private Integer embeddedAccessibilityFocusedNodeId;
215 private Integer embeddedInputFocusedNodeId;
219 private int accessibilityFeatureFlags = 0;
231 @Nullable
private SemanticsNode inputFocusedSemanticsNode;
238 @Nullable
private SemanticsNode lastInputFocusedSemanticsNode;
242 @Nullable
private SemanticsNode hoveredObject;
246 return hoveredObject.id;
254 @NonNull
private final List<Integer> flutterNavigationStack =
new ArrayList<>();
257 private int previousRouteId = ROOT_NODE_ID;
262 @NonNull
private Integer lastLeftFrameInset = 0;
264 @Nullable
private OnAccessibilityChangeListener onAccessibilityChangeListener;
272 return accessibleNavigation;
275 private boolean accessibleNavigation =
false;
277 private void setAccessibleNavigation(
boolean value) {
278 if (accessibleNavigation == value) {
281 accessibleNavigation =
value;
282 if (accessibleNavigation) {
283 accessibilityFeatureFlags |= AccessibilityFeature.ACCESSIBLE_NAVIGATION.value;
285 accessibilityFeatureFlags &= ~AccessibilityFeature.ACCESSIBLE_NAVIGATION.value;
287 sendLatestAccessibilityFlagsToFlutter();
291 private boolean isReleased =
false;
294 private final AccessibilityChannel.AccessibilityMessageHandler accessibilityMessageHandler =
295 new AccessibilityChannel.AccessibilityMessageHandler() {
298 public void announce(@NonNull String
message) {
299 rootAccessibilityView.announceForAccessibility(
message);
304 public void onTap(
int nodeId) {
305 sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_CLICKED);
310 public void onLongPress(
int nodeId) {
311 sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
316 public void onFocus(
int nodeId) {
317 sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_FOCUSED);
322 public void onTooltip(@NonNull String
message) {
329 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
332 AccessibilityEvent
e =
333 obtainAccessibilityEvent(ROOT_NODE_ID, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
335 sendAccessibilityEvent(e);
340 public void updateCustomAccessibilityActions(ByteBuffer
buffer, String[] strings) {
341 buffer.order(ByteOrder.LITTLE_ENDIAN);
347 public void updateSemantics(
348 ByteBuffer
buffer, String[] strings, ByteBuffer[] stringAttributeArgs) {
349 buffer.order(ByteOrder.LITTLE_ENDIAN);
350 for (ByteBuffer
args : stringAttributeArgs) {
351 args.order(ByteOrder.LITTLE_ENDIAN);
358 private final AccessibilityManager.AccessibilityStateChangeListener
359 accessibilityStateChangeListener =
360 new AccessibilityManager.AccessibilityStateChangeListener() {
362 public void onAccessibilityStateChanged(
boolean accessibilityEnabled) {
366 if (accessibilityEnabled) {
367 accessibilityChannel.setAccessibilityMessageHandler(accessibilityMessageHandler);
368 accessibilityChannel.onAndroidAccessibilityEnabled();
370 setAccessibleNavigation(
false);
371 accessibilityChannel.setAccessibilityMessageHandler(
null);
372 accessibilityChannel.onAndroidAccessibilityDisabled();
375 if (onAccessibilityChangeListener !=
null) {
376 onAccessibilityChangeListener.onAccessibilityChanged(
377 accessibilityEnabled, accessibilityManager.isTouchExplorationEnabled());
384 private final AccessibilityManager.TouchExplorationStateChangeListener
385 touchExplorationStateChangeListener;
389 private final ContentObserver animationScaleObserver =
390 new ContentObserver(
new Handler()) {
392 public void onChange(
boolean selfChange) {
393 this.onChange(selfChange,
null);
397 public void onChange(
boolean selfChange, Uri uri) {
403 Settings.Global.getString(
404 contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE);
406 boolean shouldAnimationsBeDisabled =
value !=
null &&
value.equals(
"0");
407 if (shouldAnimationsBeDisabled) {
408 accessibilityFeatureFlags |= AccessibilityFeature.DISABLE_ANIMATIONS.value;
410 accessibilityFeatureFlags &= ~AccessibilityFeature.DISABLE_ANIMATIONS.value;
412 sendLatestAccessibilityFlagsToFlutter();
417 @NonNull View rootAccessibilityView,
418 @NonNull AccessibilityChannel accessibilityChannel,
419 @NonNull AccessibilityManager accessibilityManager,
420 @NonNull ContentResolver contentResolver,
421 @NonNull PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) {
423 rootAccessibilityView,
424 accessibilityChannel,
425 accessibilityManager,
428 platformViewsAccessibilityDelegate);
433 @NonNull View rootAccessibilityView,
434 @NonNull AccessibilityChannel accessibilityChannel,
435 @NonNull AccessibilityManager accessibilityManager,
436 @NonNull ContentResolver contentResolver,
438 @NonNull PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) {
439 this.rootAccessibilityView = rootAccessibilityView;
440 this.accessibilityChannel = accessibilityChannel;
441 this.accessibilityManager = accessibilityManager;
442 this.contentResolver = contentResolver;
443 this.accessibilityViewEmbedder = accessibilityViewEmbedder;
444 this.platformViewsAccessibilityDelegate = platformViewsAccessibilityDelegate;
447 accessibilityStateChangeListener.onAccessibilityStateChanged(accessibilityManager.isEnabled());
448 this.accessibilityManager.addAccessibilityStateChangeListener(accessibilityStateChangeListener);
452 touchExplorationStateChangeListener =
453 new AccessibilityManager.TouchExplorationStateChangeListener() {
455 public void onTouchExplorationStateChanged(
boolean isTouchExplorationEnabled) {
459 if (!isTouchExplorationEnabled) {
460 setAccessibleNavigation(
false);
461 onTouchExplorationExit();
464 if (onAccessibilityChangeListener !=
null) {
465 onAccessibilityChangeListener.onAccessibilityChanged(
466 accessibilityManager.isEnabled(), isTouchExplorationEnabled);
470 touchExplorationStateChangeListener.onTouchExplorationStateChanged(
471 accessibilityManager.isTouchExplorationEnabled());
472 this.accessibilityManager.addTouchExplorationStateChangeListener(
473 touchExplorationStateChangeListener);
477 animationScaleObserver.onChange(
false);
478 Uri transitionUri = Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE);
479 this.contentResolver.registerContentObserver(transitionUri,
false, animationScaleObserver);
483 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_31) {
487 platformViewsAccessibilityDelegate.attachAccessibilityBridge(
this);
497 public void release() {
499 platformViewsAccessibilityDelegate.detachAccessibilityBridge();
500 setOnAccessibilityChangeListener(
null);
501 accessibilityManager.removeAccessibilityStateChangeListener(accessibilityStateChangeListener);
502 accessibilityManager.removeTouchExplorationStateChangeListener(
503 touchExplorationStateChangeListener);
504 contentResolver.unregisterContentObserver(animationScaleObserver);
505 accessibilityChannel.setAccessibilityMessageHandler(
null);
509 public boolean isAccessibilityEnabled() {
510 return accessibilityManager.isEnabled();
514 public boolean isTouchExplorationEnabled() {
515 return accessibilityManager.isTouchExplorationEnabled();
522 public void setOnAccessibilityChangeListener(@Nullable OnAccessibilityChangeListener listener) {
523 this.onAccessibilityChangeListener = listener;
527 private void sendLatestAccessibilityFlagsToFlutter() {
528 accessibilityChannel.setAccessibilityFeatures(accessibilityFeatureFlags);
531 private boolean shouldSetCollectionInfo(
final SemanticsNode semanticsNode) {
541 return semanticsNode.scrollChildren > 0
542 && (SemanticsNode.nullableHasAncestor(
543 accessibilityFocusedSemanticsNode, o -> o == semanticsNode)
544 || !SemanticsNode.nullableHasAncestor(
545 accessibilityFocusedSemanticsNode, o -> o.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)));
548 @TargetApi(API_LEVELS.API_31)
549 @RequiresApi(API_LEVELS.API_31)
550 private
void setBoldTextFlag() {
551 if (rootAccessibilityView ==
null || rootAccessibilityView.getResources() ==
null) {
554 int fontWeightAdjustment =
555 rootAccessibilityView.getResources().getConfiguration().fontWeightAdjustment;
557 fontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED
558 && fontWeightAdjustment >= BOLD_TEXT_WEIGHT_ADJUSTMENT;
561 accessibilityFeatureFlags |= AccessibilityFeature.BOLD_TEXT.value;
563 accessibilityFeatureFlags &= AccessibilityFeature.BOLD_TEXT.value;
565 sendLatestAccessibilityFlagsToFlutter();
569 public AccessibilityNodeInfo obtainAccessibilityNodeInfo(View rootView) {
570 return AccessibilityNodeInfo.obtain(rootView);
574 public AccessibilityNodeInfo obtainAccessibilityNodeInfo(View rootView,
int virtualViewId) {
575 return AccessibilityNodeInfo.obtain(rootView, virtualViewId);
602 @SuppressWarnings(
"deprecation")
604 @SuppressLint(
"NewApi")
605 public AccessibilityNodeInfo createAccessibilityNodeInfo(
int virtualViewId) {
606 setAccessibleNavigation(
true);
607 if (virtualViewId >= MIN_ENGINE_GENERATED_NODE_ID) {
610 return accessibilityViewEmbedder.createAccessibilityNodeInfo(virtualViewId);
613 if (virtualViewId == View.NO_ID) {
614 AccessibilityNodeInfo
result = obtainAccessibilityNodeInfo(rootAccessibilityView);
615 rootAccessibilityView.onInitializeAccessibilityNodeInfo(
result);
618 if (flutterSemanticsTree.containsKey(ROOT_NODE_ID)) {
619 result.addChild(rootAccessibilityView, ROOT_NODE_ID);
621 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_24) {
622 result.setImportantForAccessibility(
false);
627 SemanticsNode semanticsNode = flutterSemanticsTree.get(virtualViewId);
628 if (semanticsNode ==
null) {
640 if (semanticsNode.platformViewId != -1) {
641 if (platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
643 platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
644 if (embeddedView ==
null) {
648 return accessibilityViewEmbedder.getRootNode(embeddedView, semanticsNode.id, bounds);
652 AccessibilityNodeInfo
result =
653 obtainAccessibilityNodeInfo(rootAccessibilityView, virtualViewId);
657 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_24) {
658 result.setImportantForAccessibility(isImportant(semanticsNode));
662 result.setViewIdResourceName(
"");
663 if (semanticsNode.identifier !=
null) {
664 result.setViewIdResourceName(semanticsNode.identifier);
666 result.setPackageName(rootAccessibilityView.getContext().getPackageName());
667 result.setClassName(
"android.view.View");
668 result.setSource(rootAccessibilityView, virtualViewId);
669 result.setFocusable(semanticsNode.isFocusable());
670 if (inputFocusedSemanticsNode !=
null) {
671 result.setFocused(inputFocusedSemanticsNode.id == virtualViewId);
674 if (accessibilityFocusedSemanticsNode !=
null) {
675 result.setAccessibilityFocused(accessibilityFocusedSemanticsNode.id == virtualViewId);
678 if (semanticsNode.hasFlag(Flag.IS_TEXT_FIELD)) {
679 result.setPassword(semanticsNode.hasFlag(Flag.IS_OBSCURED));
680 if (!semanticsNode.hasFlag(Flag.IS_READ_ONLY)) {
681 result.setClassName(
"android.widget.EditText");
683 result.setEditable(!semanticsNode.hasFlag(Flag.IS_READ_ONLY));
684 if (semanticsNode.textSelectionBase != -1 && semanticsNode.textSelectionExtent != -1) {
685 result.setTextSelection(semanticsNode.textSelectionBase, semanticsNode.textSelectionExtent);
690 if (accessibilityFocusedSemanticsNode !=
null
691 && accessibilityFocusedSemanticsNode.id == virtualViewId) {
692 result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
696 int granularities = 0;
697 if (semanticsNode.hasAction(
Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) {
698 result.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
699 granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER;
701 if (semanticsNode.hasAction(
Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) {
702 result.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
703 granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER;
705 if (semanticsNode.hasAction(
Action.MOVE_CURSOR_FORWARD_BY_WORD)) {
706 result.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
707 granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD;
709 if (semanticsNode.hasAction(
Action.MOVE_CURSOR_BACKWARD_BY_WORD)) {
710 result.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
711 granularities |= AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD;
713 result.setMovementGranularities(granularities);
714 if (semanticsNode.maxValueLength >= 0) {
717 final int length = semanticsNode.value ==
null ? 0 : semanticsNode.value.length();
718 int a =
length - semanticsNode.currentValueLength + semanticsNode.maxValueLength;
720 length - semanticsNode.currentValueLength + semanticsNode.maxValueLength);
726 if (semanticsNode.hasAction(
Action.SET_SELECTION)) {
727 result.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
729 if (semanticsNode.hasAction(
Action.COPY)) {
730 result.addAction(AccessibilityNodeInfo.ACTION_COPY);
732 if (semanticsNode.hasAction(
Action.CUT)) {
733 result.addAction(AccessibilityNodeInfo.ACTION_CUT);
735 if (semanticsNode.hasAction(
Action.PASTE)) {
736 result.addAction(AccessibilityNodeInfo.ACTION_PASTE);
739 if (semanticsNode.hasAction(
Action.SET_TEXT)) {
740 result.addAction(AccessibilityNodeInfo.ACTION_SET_TEXT);
743 if (semanticsNode.hasFlag(Flag.IS_BUTTON) || semanticsNode.hasFlag(Flag.IS_LINK)) {
744 result.setClassName(
"android.widget.Button");
746 if (semanticsNode.hasFlag(Flag.IS_IMAGE)) {
747 result.setClassName(
"android.widget.ImageView");
751 if (semanticsNode.hasAction(
Action.DISMISS)) {
752 result.setDismissable(
true);
753 result.addAction(AccessibilityNodeInfo.ACTION_DISMISS);
756 if (semanticsNode.parent !=
null) {
757 if (BuildConfig.DEBUG && semanticsNode.id <= ROOT_NODE_ID) {
758 Log.e(TAG,
"Semantics node id is not > ROOT_NODE_ID.");
760 result.setParent(rootAccessibilityView, semanticsNode.parent.id);
762 if (BuildConfig.DEBUG && semanticsNode.id != ROOT_NODE_ID) {
763 Log.e(TAG,
"Semantics node id does not equal ROOT_NODE_ID.");
765 result.setParent(rootAccessibilityView);
768 if (semanticsNode.previousNodeId != -1 &&
Build.VERSION.SDK_INT >= API_LEVELS.API_22) {
769 result.setTraversalAfter(rootAccessibilityView, semanticsNode.previousNodeId);
773 if (semanticsNode.parent !=
null) {
774 Rect parentBounds = semanticsNode.parent.getGlobalRect();
775 Rect boundsInParent =
new Rect(bounds);
776 boundsInParent.offset(-parentBounds.left, -parentBounds.top);
777 result.setBoundsInParent(boundsInParent);
779 result.setBoundsInParent(bounds);
781 final Rect boundsInScreen = getBoundsInScreen(bounds);
782 result.setBoundsInScreen(boundsInScreen);
783 result.setVisibleToUser(
true);
785 !semanticsNode.hasFlag(Flag.HAS_ENABLED_STATE) || semanticsNode.hasFlag(Flag.IS_ENABLED));
787 if (semanticsNode.hasAction(
Action.TAP)) {
788 if (semanticsNode.onTapOverride !=
null) {
790 new AccessibilityNodeInfo.AccessibilityAction(
791 AccessibilityNodeInfo.ACTION_CLICK, semanticsNode.onTapOverride.hint));
792 result.setClickable(
true);
794 result.addAction(AccessibilityNodeInfo.ACTION_CLICK);
795 result.setClickable(
true);
798 if (semanticsNode.hasAction(
Action.LONG_PRESS)) {
799 if (semanticsNode.onLongPressOverride !=
null) {
801 new AccessibilityNodeInfo.AccessibilityAction(
802 AccessibilityNodeInfo.ACTION_LONG_CLICK, semanticsNode.onLongPressOverride.hint));
803 result.setLongClickable(
true);
805 result.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
806 result.setLongClickable(
true);
809 if (semanticsNode.hasAction(
Action.SCROLL_LEFT)
810 || semanticsNode.hasAction(
Action.SCROLL_UP)
811 || semanticsNode.hasAction(
Action.SCROLL_RIGHT)
812 || semanticsNode.hasAction(
Action.SCROLL_DOWN)) {
813 result.setScrollable(
true);
829 if (semanticsNode.hasFlag(Flag.HAS_IMPLICIT_SCROLLING)) {
830 if (semanticsNode.hasAction(
Action.SCROLL_LEFT)
831 || semanticsNode.hasAction(
Action.SCROLL_RIGHT)) {
832 if (shouldSetCollectionInfo(semanticsNode)) {
834 AccessibilityNodeInfo.CollectionInfo.obtain(
836 semanticsNode.scrollChildren,
840 result.setClassName(
"android.widget.HorizontalScrollView");
843 if (shouldSetCollectionInfo(semanticsNode)) {
845 AccessibilityNodeInfo.CollectionInfo.obtain(
846 semanticsNode.scrollChildren,
851 result.setClassName(
"android.widget.ScrollView");
858 if (semanticsNode.hasAction(
Action.SCROLL_LEFT)
859 || semanticsNode.hasAction(
Action.SCROLL_UP)) {
860 result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
862 if (semanticsNode.hasAction(
Action.SCROLL_RIGHT)
863 || semanticsNode.hasAction(
Action.SCROLL_DOWN)) {
864 result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
867 if (semanticsNode.hasAction(
Action.INCREASE) || semanticsNode.hasAction(
Action.DECREASE)) {
870 result.setClassName(
"android.widget.SeekBar");
871 if (semanticsNode.hasAction(
Action.INCREASE)) {
872 result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
874 if (semanticsNode.hasAction(
Action.DECREASE)) {
875 result.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
878 if (semanticsNode.hasFlag(Flag.IS_LIVE_REGION)) {
879 result.setLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
884 if (semanticsNode.hasFlag(Flag.IS_TEXT_FIELD)) {
885 result.setText(semanticsNode.getValue());
886 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
887 result.setHintText(semanticsNode.getTextFieldHint());
889 }
else if (!semanticsNode.hasFlag(Flag.SCOPES_ROUTE)) {
890 CharSequence
content = semanticsNode.getValueLabelHint();
891 if (
Build.VERSION.SDK_INT < API_LEVELS.API_28) {
892 if (semanticsNode.tooltip !=
null) {
904 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
905 if (semanticsNode.tooltip !=
null) {
906 result.setTooltipText(semanticsNode.tooltip);
910 boolean hasCheckedState = semanticsNode.hasFlag(Flag.HAS_CHECKED_STATE);
911 boolean hasToggledState = semanticsNode.hasFlag(Flag.HAS_TOGGLED_STATE);
912 if (BuildConfig.DEBUG && (hasCheckedState && hasToggledState)) {
913 Log.e(TAG,
"Expected semanticsNode to have checked state and toggled state.");
915 result.setCheckable(hasCheckedState || hasToggledState);
916 if (hasCheckedState) {
917 result.setChecked(semanticsNode.hasFlag(Flag.IS_CHECKED));
918 if (semanticsNode.hasFlag(Flag.IS_IN_MUTUALLY_EXCLUSIVE_GROUP)) {
919 result.setClassName(
"android.widget.RadioButton");
921 result.setClassName(
"android.widget.CheckBox");
923 }
else if (hasToggledState) {
924 result.setChecked(semanticsNode.hasFlag(Flag.IS_TOGGLED));
925 result.setClassName(
"android.widget.Switch");
927 result.setSelected(semanticsNode.hasFlag(Flag.IS_SELECTED));
930 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
931 result.setHeading(semanticsNode.hasFlag(Flag.IS_HEADER));
935 if (accessibilityFocusedSemanticsNode !=
null
936 && accessibilityFocusedSemanticsNode.id == virtualViewId) {
937 result.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
939 result.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
943 if (semanticsNode.customAccessibilityActions !=
null) {
944 for (CustomAccessibilityAction
action : semanticsNode.customAccessibilityActions) {
946 new AccessibilityNodeInfo.AccessibilityAction(
action.resourceId,
action.label));
950 for (SemanticsNode child : semanticsNode.childrenInTraversalOrder) {
951 if (child.hasFlag(Flag.IS_HIDDEN)) {
954 if (child.platformViewId != -1) {
956 platformViewsAccessibilityDelegate.getPlatformViewById(child.platformViewId);
965 if (!platformViewsAccessibilityDelegate.usesVirtualDisplay(child.platformViewId)) {
966 result.addChild(embeddedView);
970 result.addChild(rootAccessibilityView, child.id);
975 private boolean isImportant(SemanticsNode node) {
976 if (node.hasFlag(Flag.SCOPES_ROUTE)) {
980 if (node.getValueLabelHint() !=
null) {
985 return (node.actions & ~systemAction) != 0;
994 private Rect getBoundsInScreen(Rect bounds) {
995 Rect boundsInScreen =
new Rect(bounds);
996 int[] locationOnScreen =
new int[2];
997 rootAccessibilityView.getLocationOnScreen(locationOnScreen);
998 boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
999 return boundsInScreen;
1016 public boolean performAction(
1017 int virtualViewId,
int accessibilityAction, @Nullable Bundle arguments) {
1018 if (virtualViewId >= MIN_ENGINE_GENERATED_NODE_ID) {
1021 boolean didPerform =
1022 accessibilityViewEmbedder.performAction(virtualViewId, accessibilityAction, arguments);
1024 && accessibilityAction == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
1025 embeddedAccessibilityFocusedNodeId =
null;
1029 SemanticsNode semanticsNode = flutterSemanticsTree.get(virtualViewId);
1030 if (semanticsNode ==
null) {
1033 switch (accessibilityAction) {
1034 case AccessibilityNodeInfo.ACTION_CLICK:
1039 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.TAP);
1042 case AccessibilityNodeInfo.ACTION_LONG_CLICK:
1047 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.LONG_PRESS);
1050 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
1052 if (semanticsNode.hasAction(
Action.SCROLL_UP)) {
1053 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.SCROLL_UP);
1054 }
else if (semanticsNode.hasAction(
Action.SCROLL_LEFT)) {
1056 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.SCROLL_LEFT);
1057 }
else if (semanticsNode.hasAction(
Action.INCREASE)) {
1058 semanticsNode.value = semanticsNode.increasedValue;
1059 semanticsNode.valueAttributes = semanticsNode.increasedValueAttributes;
1061 sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
1062 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.INCREASE);
1068 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
1070 if (semanticsNode.hasAction(
Action.SCROLL_DOWN)) {
1071 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.SCROLL_DOWN);
1072 }
else if (semanticsNode.hasAction(
Action.SCROLL_RIGHT)) {
1074 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.SCROLL_RIGHT);
1075 }
else if (semanticsNode.hasAction(
Action.DECREASE)) {
1076 semanticsNode.value = semanticsNode.decreasedValue;
1077 semanticsNode.valueAttributes = semanticsNode.decreasedValueAttributes;
1079 sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
1080 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.DECREASE);
1086 case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY:
1088 return performCursorMoveAction(semanticsNode, virtualViewId, arguments,
false);
1090 case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY:
1092 return performCursorMoveAction(semanticsNode, virtualViewId, arguments,
true);
1094 case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
1099 if (accessibilityFocusedSemanticsNode !=
null
1100 && accessibilityFocusedSemanticsNode.id == virtualViewId) {
1101 accessibilityFocusedSemanticsNode =
null;
1103 if (embeddedAccessibilityFocusedNodeId !=
null
1104 && embeddedAccessibilityFocusedNodeId == virtualViewId) {
1105 embeddedAccessibilityFocusedNodeId =
null;
1107 accessibilityChannel.dispatchSemanticsAction(
1108 virtualViewId,
Action.DID_LOSE_ACCESSIBILITY_FOCUS);
1109 sendAccessibilityEvent(
1110 virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
1113 case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
1115 if (accessibilityFocusedSemanticsNode ==
null) {
1119 rootAccessibilityView.invalidate();
1123 accessibilityFocusedSemanticsNode = semanticsNode;
1125 accessibilityChannel.dispatchSemanticsAction(
1126 virtualViewId,
Action.DID_GAIN_ACCESSIBILITY_FOCUS);
1128 HashMap<String, Object>
message =
new HashMap<>();
1129 message.put(
"type",
"didGainFocus");
1130 message.put(
"nodeId", semanticsNode.id);
1131 accessibilityChannel.channel.send(
message);
1133 sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
1135 if (semanticsNode.hasAction(
Action.INCREASE)
1136 || semanticsNode.hasAction(
Action.DECREASE)) {
1138 sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_SELECTED);
1143 case ACTION_SHOW_ON_SCREEN:
1145 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.SHOW_ON_SCREEN);
1148 case AccessibilityNodeInfo.ACTION_SET_SELECTION:
1150 final Map<String, Integer> selection =
new HashMap<>();
1151 final boolean hasSelection =
1153 && arguments.containsKey(
1154 AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT)
1155 && arguments.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT);
1159 arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT));
1162 arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT));
1165 selection.put(
"base", semanticsNode.textSelectionExtent);
1166 selection.put(
"extent", semanticsNode.textSelectionExtent);
1168 accessibilityChannel.dispatchSemanticsAction(
1169 virtualViewId,
Action.SET_SELECTION, selection);
1173 SemanticsNode node = flutterSemanticsTree.get(virtualViewId);
1174 node.textSelectionBase = selection.get(
"base");
1175 node.textSelectionExtent = selection.get(
"extent");
1178 case AccessibilityNodeInfo.ACTION_COPY:
1180 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.COPY);
1183 case AccessibilityNodeInfo.ACTION_CUT:
1185 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.CUT);
1188 case AccessibilityNodeInfo.ACTION_PASTE:
1190 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.PASTE);
1193 case AccessibilityNodeInfo.ACTION_DISMISS:
1195 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.DISMISS);
1198 case AccessibilityNodeInfo.ACTION_SET_TEXT:
1200 return performSetText(semanticsNode, virtualViewId, arguments);
1204 final int flutterId = accessibilityAction - FIRST_RESOURCE_ID;
1205 CustomAccessibilityAction contextAction = customAccessibilityActions.get(flutterId);
1206 if (contextAction !=
null) {
1207 accessibilityChannel.dispatchSemanticsAction(
1208 virtualViewId,
Action.CUSTOM_ACTION, contextAction.id);
1219 private boolean performCursorMoveAction(
1220 @NonNull SemanticsNode semanticsNode,
1222 @NonNull Bundle arguments,
1224 final int granularity =
1225 arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
1226 final boolean extendSelection =
1227 arguments.getBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
1230 final int previousTextSelectionBase = semanticsNode.textSelectionBase;
1231 final int previousTextSelectionExtent = semanticsNode.textSelectionExtent;
1232 predictCursorMovement(semanticsNode, granularity, forward, extendSelection);
1234 if (previousTextSelectionBase != semanticsNode.textSelectionBase
1235 || previousTextSelectionExtent != semanticsNode.textSelectionExtent) {
1236 final String
value = semanticsNode.value !=
null ? semanticsNode.value :
"";
1237 final AccessibilityEvent selectionEvent =
1238 obtainAccessibilityEvent(
1239 semanticsNode.id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
1240 selectionEvent.getText().add(value);
1241 selectionEvent.setFromIndex(semanticsNode.textSelectionBase);
1242 selectionEvent.setToIndex(semanticsNode.textSelectionExtent);
1243 selectionEvent.setItemCount(
value.length());
1244 sendAccessibilityEvent(selectionEvent);
1247 switch (granularity) {
1248 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER:
1250 if (forward && semanticsNode.hasAction(
Action.MOVE_CURSOR_FORWARD_BY_CHARACTER)) {
1251 accessibilityChannel.dispatchSemanticsAction(
1252 virtualViewId,
Action.MOVE_CURSOR_FORWARD_BY_CHARACTER, extendSelection);
1255 if (!forward && semanticsNode.hasAction(
Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER)) {
1256 accessibilityChannel.dispatchSemanticsAction(
1257 virtualViewId,
Action.MOVE_CURSOR_BACKWARD_BY_CHARACTER, extendSelection);
1262 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD:
1263 if (forward && semanticsNode.hasAction(
Action.MOVE_CURSOR_FORWARD_BY_WORD)) {
1264 accessibilityChannel.dispatchSemanticsAction(
1265 virtualViewId,
Action.MOVE_CURSOR_FORWARD_BY_WORD, extendSelection);
1268 if (!forward && semanticsNode.hasAction(
Action.MOVE_CURSOR_BACKWARD_BY_WORD)) {
1269 accessibilityChannel.dispatchSemanticsAction(
1270 virtualViewId,
Action.MOVE_CURSOR_BACKWARD_BY_WORD, extendSelection);
1274 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE:
1275 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH:
1276 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE:
1282 private void predictCursorMovement(
1283 @NonNull SemanticsNode node,
int granularity,
boolean forward,
boolean extendSelection) {
1284 if (node.textSelectionExtent < 0 || node.textSelectionBase < 0) {
1288 switch (granularity) {
1289 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER:
1290 if (forward && node.textSelectionExtent < node.value.length()) {
1291 node.textSelectionExtent += 1;
1292 }
else if (!forward && node.textSelectionExtent > 0) {
1293 node.textSelectionExtent -= 1;
1296 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD:
1297 if (forward && node.textSelectionExtent < node.value.length()) {
1298 Pattern pattern = Pattern.compile(
"\\p{L}(\\b)");
1299 Matcher
result = pattern.matcher(node.value.substring(node.textSelectionExtent));
1303 node.textSelectionExtent +=
result.start(1);
1305 node.textSelectionExtent = node.value.length();
1307 }
else if (!forward && node.textSelectionExtent > 0) {
1309 Pattern pattern = Pattern.compile(
"(?s:.*)(\\b)\\p{L}");
1310 Matcher
result = pattern.matcher(node.value.substring(0, node.textSelectionExtent));
1312 node.textSelectionExtent =
result.start(1);
1316 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE:
1317 if (forward && node.textSelectionExtent < node.value.length()) {
1319 Pattern pattern = Pattern.compile(
"(?!^)(\\n)");
1320 Matcher
result = pattern.matcher(node.value.substring(node.textSelectionExtent));
1322 node.textSelectionExtent +=
result.start(1);
1324 node.textSelectionExtent = node.value.length();
1326 }
else if (!forward && node.textSelectionExtent > 0) {
1328 Pattern pattern = Pattern.compile(
"(?s:.*)(\\n)");
1329 Matcher
result = pattern.matcher(node.value.substring(0, node.textSelectionExtent));
1331 node.textSelectionExtent =
result.start(1);
1333 node.textSelectionExtent = 0;
1337 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH:
1338 case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE:
1340 node.textSelectionExtent = node.value.length();
1342 node.textSelectionExtent = 0;
1346 if (!extendSelection) {
1347 node.textSelectionBase = node.textSelectionExtent;
1355 private boolean performSetText(SemanticsNode node,
int virtualViewId, @NonNull Bundle arguments) {
1356 String newText =
"";
1357 if (arguments !=
null
1358 && arguments.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE)) {
1359 newText = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
1361 accessibilityChannel.dispatchSemanticsAction(virtualViewId,
Action.SET_TEXT, newText);
1364 node.value = newText;
1365 node.valueAttributes =
null;
1385 public AccessibilityNodeInfo findFocus(
int focus) {
1387 case AccessibilityNodeInfo.FOCUS_INPUT:
1389 if (inputFocusedSemanticsNode !=
null) {
1390 return createAccessibilityNodeInfo(inputFocusedSemanticsNode.id);
1392 if (embeddedInputFocusedNodeId !=
null) {
1393 return createAccessibilityNodeInfo(embeddedInputFocusedNodeId);
1397 case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY:
1399 if (accessibilityFocusedSemanticsNode !=
null) {
1400 return createAccessibilityNodeInfo(accessibilityFocusedSemanticsNode.id);
1402 if (embeddedAccessibilityFocusedNodeId !=
null) {
1403 return createAccessibilityNodeInfo(embeddedAccessibilityFocusedNodeId);
1411 private SemanticsNode getRootSemanticsNode() {
1412 if (BuildConfig.DEBUG && !flutterSemanticsTree.containsKey(0)) {
1413 Log.e(TAG,
"Attempted to getRootSemanticsNode without a root semantics node.");
1415 return flutterSemanticsTree.get(0);
1428 private SemanticsNode getOrCreateSemanticsNode(
int id) {
1429 SemanticsNode semanticsNode = flutterSemanticsTree.get(
id);
1430 if (semanticsNode ==
null) {
1431 semanticsNode =
new SemanticsNode(
this);
1432 semanticsNode.id =
id;
1433 flutterSemanticsTree.put(
id, semanticsNode);
1435 return semanticsNode;
1449 private CustomAccessibilityAction getOrCreateAccessibilityAction(
int id) {
1450 CustomAccessibilityAction
action = customAccessibilityActions.get(
id);
1452 action =
new CustomAccessibilityAction();
1454 action.resourceId =
id + FIRST_RESOURCE_ID;
1455 customAccessibilityActions.put(
id,
action);
1470 public boolean onAccessibilityHoverEvent(MotionEvent
event) {
1471 return onAccessibilityHoverEvent(
event,
false);
1488 public boolean onAccessibilityHoverEvent(MotionEvent
event,
boolean ignorePlatformViews) {
1489 if (!accessibilityManager.isTouchExplorationEnabled()) {
1492 if (flutterSemanticsTree.isEmpty()) {
1496 SemanticsNode semanticsNodeUnderCursor =
1497 getRootSemanticsNode()
1498 .hitTest(
new float[] {
event.getX(),
event.getY(), 0, 1}, ignorePlatformViews);
1501 if (semanticsNodeUnderCursor !=
null && semanticsNodeUnderCursor.platformViewId != -1) {
1502 if (ignorePlatformViews) {
1505 return accessibilityViewEmbedder.onAccessibilityHoverEvent(
1506 semanticsNodeUnderCursor.id,
event);
1509 if (
event.getAction() == MotionEvent.ACTION_HOVER_ENTER
1510 ||
event.getAction() == MotionEvent.ACTION_HOVER_MOVE) {
1511 handleTouchExploration(
event.getX(),
event.getY(), ignorePlatformViews);
1512 }
else if (
event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
1513 onTouchExplorationExit();
1515 Log.d(
"flutter",
"unexpected accessibility hover event: " +
event);
1528 private void onTouchExplorationExit() {
1529 if (hoveredObject !=
null) {
1530 sendAccessibilityEvent(hoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
1531 hoveredObject =
null;
1544 private void handleTouchExploration(
float x,
float y,
boolean ignorePlatformViews) {
1545 if (flutterSemanticsTree.isEmpty()) {
1548 SemanticsNode semanticsNodeUnderCursor =
1549 getRootSemanticsNode().hitTest(
new float[] {
x,
y, 0, 1}, ignorePlatformViews);
1550 if (semanticsNodeUnderCursor != hoveredObject) {
1552 if (semanticsNodeUnderCursor !=
null) {
1553 sendAccessibilityEvent(
1554 semanticsNodeUnderCursor.id, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
1556 if (hoveredObject !=
null) {
1557 sendAccessibilityEvent(hoveredObject.id, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
1559 hoveredObject = semanticsNodeUnderCursor;
1572 void updateCustomAccessibilityActions(@NonNull ByteBuffer
buffer, @NonNull String[] strings) {
1573 while (
buffer.hasRemaining()) {
1574 int id =
buffer.getInt();
1575 CustomAccessibilityAction
action = getOrCreateAccessibilityAction(
id);
1576 action.overrideId =
buffer.getInt();
1577 int stringIndex =
buffer.getInt();
1578 action.label = stringIndex == -1 ? null : strings[stringIndex];
1579 stringIndex =
buffer.getInt();
1580 action.hint = stringIndex == -1 ? null : strings[stringIndex];
1591 void updateSemantics(
1592 @NonNull ByteBuffer
buffer,
1593 @NonNull String[] strings,
1594 @NonNull ByteBuffer[] stringAttributeArgs) {
1595 ArrayList<SemanticsNode> updated =
new ArrayList<>();
1596 while (
buffer.hasRemaining()) {
1597 int id =
buffer.getInt();
1598 SemanticsNode semanticsNode = getOrCreateSemanticsNode(
id);
1599 semanticsNode.updateWith(
buffer, strings, stringAttributeArgs);
1600 if (semanticsNode.hasFlag(Flag.IS_HIDDEN)) {
1603 if (semanticsNode.hasFlag(Flag.IS_FOCUSED)) {
1604 inputFocusedSemanticsNode = semanticsNode;
1606 if (semanticsNode.hadPreviousConfig) {
1607 updated.add(semanticsNode);
1609 if (semanticsNode.platformViewId != -1
1610 && !platformViewsAccessibilityDelegate.usesVirtualDisplay(semanticsNode.platformViewId)) {
1612 platformViewsAccessibilityDelegate.getPlatformViewById(semanticsNode.platformViewId);
1613 if (embeddedView !=
null) {
1614 embeddedView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
1619 Set<SemanticsNode> visitedObjects =
new HashSet<>();
1620 SemanticsNode rootObject = getRootSemanticsNode();
1622 if (rootObject !=
null) {
1623 final float[]
identity =
new float[16];
1624 Matrix.setIdentityM(identity, 0);
1628 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_23) {
1629 boolean needsToApplyLeftCutoutInset =
true;
1634 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
1635 needsToApplyLeftCutoutInset = doesLayoutInDisplayCutoutModeRequireLeftInset();
1638 if (needsToApplyLeftCutoutInset) {
1639 WindowInsets insets = rootAccessibilityView.getRootWindowInsets();
1640 if (insets !=
null) {
1641 if (!lastLeftFrameInset.equals(insets.getSystemWindowInsetLeft())) {
1642 rootObject.globalGeometryDirty =
true;
1643 rootObject.inverseTransformDirty =
true;
1645 lastLeftFrameInset = insets.getSystemWindowInsetLeft();
1646 Matrix.translateM(identity, 0, lastLeftFrameInset, 0, 0);
1650 rootObject.updateRecursively(identity, visitedObjects,
false);
1651 rootObject.collectRoutes(newRoutes);
1658 SemanticsNode lastAdded =
null;
1659 for (SemanticsNode semanticsNode : newRoutes) {
1660 if (!flutterNavigationStack.contains(semanticsNode.id)) {
1661 lastAdded = semanticsNode;
1666 if (lastAdded ==
null && newRoutes.size() > 0) {
1667 lastAdded = newRoutes.get(newRoutes.size() - 1);
1677 if (lastAdded !=
null
1678 && (lastAdded.id != previousRouteId || newRoutes.size() != flutterNavigationStack.size())) {
1679 previousRouteId = lastAdded.id;
1680 onWindowNameChange(lastAdded);
1682 flutterNavigationStack.clear();
1683 for (SemanticsNode semanticsNode : newRoutes) {
1684 flutterNavigationStack.add(semanticsNode.id);
1687 Iterator<Map.Entry<Integer, SemanticsNode>> it = flutterSemanticsTree.entrySet().iterator();
1688 while (it.hasNext()) {
1689 Map.Entry<Integer, SemanticsNode> entry = it.next();
1690 SemanticsNode
object = entry.getValue();
1691 if (!visitedObjects.contains(
object)) {
1692 willRemoveSemanticsNode(
object);
1699 sendWindowContentChangeEvent(0);
1701 for (SemanticsNode
object : updated) {
1702 if (
object.didScroll()) {
1703 AccessibilityEvent
event =
1704 obtainAccessibilityEvent(
object.
id, AccessibilityEvent.TYPE_VIEW_SCROLLED);
1708 float position =
object.scrollPosition;
1709 float max =
object.scrollExtentMax;
1710 if (Float.isInfinite(
object.scrollExtentMax)) {
1711 max = SCROLL_EXTENT_FOR_INFINITY;
1712 if (position > SCROLL_POSITION_CAP_FOR_INFINITY) {
1713 position = SCROLL_POSITION_CAP_FOR_INFINITY;
1716 if (Float.isInfinite(
object.scrollExtentMin)) {
1717 max += SCROLL_EXTENT_FOR_INFINITY;
1718 if (position < -SCROLL_POSITION_CAP_FOR_INFINITY) {
1719 position = -SCROLL_POSITION_CAP_FOR_INFINITY;
1721 position += SCROLL_EXTENT_FOR_INFINITY;
1723 max -=
object.scrollExtentMin;
1724 position -=
object.scrollExtentMin;
1727 if (
object.hadAction(
Action.SCROLL_UP) ||
object.hadAction(
Action.SCROLL_DOWN)) {
1728 event.setScrollY((
int) position);
1729 event.setMaxScrollY((
int)
max);
1730 }
else if (
object.hadAction(
Action.SCROLL_LEFT) ||
object.hadAction(
Action.SCROLL_RIGHT)) {
1731 event.setScrollX((
int) position);
1732 event.setMaxScrollX((
int)
max);
1734 if (
object.scrollChildren > 0) {
1736 event.setItemCount(
object.scrollChildren);
1737 event.setFromIndex(
object.scrollIndex);
1738 int visibleChildren = 0;
1740 for (SemanticsNode child : object.childrenInHitTestOrder) {
1741 if (!child.hasFlag(Flag.IS_HIDDEN)) {
1742 visibleChildren += 1;
1745 if (BuildConfig.DEBUG) {
1746 if (
object.scrollIndex + visibleChildren >
object.scrollChildren) {
1747 Log.e(TAG,
"Scroll index is out of bounds.");
1750 if (
object.childrenInHitTestOrder.isEmpty()) {
1751 Log.e(TAG,
"Had scrollChildren but no childrenInHitTestOrder");
1762 event.setToIndex(
object.scrollIndex + visibleChildren - 1);
1764 sendAccessibilityEvent(
event);
1766 if (
object.hasFlag(Flag.IS_LIVE_REGION) &&
object.didChangeLabel()) {
1767 sendWindowContentChangeEvent(
object.
id);
1769 if (accessibilityFocusedSemanticsNode !=
null
1770 && accessibilityFocusedSemanticsNode.id ==
object.id
1771 && !
object.hadFlag(Flag.IS_SELECTED)
1772 &&
object.hasFlag(Flag.IS_SELECTED)) {
1773 AccessibilityEvent
event =
1774 obtainAccessibilityEvent(
object.
id, AccessibilityEvent.TYPE_VIEW_SELECTED);
1775 event.getText().add(
object.label);
1776 sendAccessibilityEvent(
event);
1781 if (inputFocusedSemanticsNode !=
null
1782 && inputFocusedSemanticsNode.id ==
object.id
1783 && (lastInputFocusedSemanticsNode ==
null
1784 || lastInputFocusedSemanticsNode.id != inputFocusedSemanticsNode.id)) {
1785 lastInputFocusedSemanticsNode = inputFocusedSemanticsNode;
1786 sendAccessibilityEvent(
1787 obtainAccessibilityEvent(
object.
id, AccessibilityEvent.TYPE_VIEW_FOCUSED));
1788 }
else if (inputFocusedSemanticsNode ==
null) {
1792 lastInputFocusedSemanticsNode =
null;
1795 if (inputFocusedSemanticsNode !=
null
1796 && inputFocusedSemanticsNode.id ==
object.id
1797 &&
object.hadFlag(Flag.IS_TEXT_FIELD)
1798 &&
object.hasFlag(Flag.IS_TEXT_FIELD)
1803 && (accessibilityFocusedSemanticsNode ==
null
1804 || (accessibilityFocusedSemanticsNode.id == inputFocusedSemanticsNode.id))) {
1805 String oldValue = object.previousValue !=
null ? object.previousValue :
"";
1806 String newValue = object.value !=
null ? object.value :
"";
1807 AccessibilityEvent
event = createTextChangedEvent(
object.
id, oldValue, newValue);
1808 if (
event !=
null) {
1809 sendAccessibilityEvent(
event);
1812 if (
object.previousTextSelectionBase !=
object.textSelectionBase
1813 ||
object.previousTextSelectionExtent !=
object.textSelectionExtent) {
1814 AccessibilityEvent selectionEvent =
1815 obtainAccessibilityEvent(
1816 object.
id, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
1817 selectionEvent.getText().add(newValue);
1818 selectionEvent.setFromIndex(
object.textSelectionBase);
1819 selectionEvent.setToIndex(
object.textSelectionExtent);
1820 selectionEvent.setItemCount(newValue.length());
1821 sendAccessibilityEvent(selectionEvent);
1827 private AccessibilityEvent createTextChangedEvent(
int id, String oldValue, String newValue) {
1828 AccessibilityEvent
e = obtainAccessibilityEvent(
id, AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
1829 e.setBeforeText(oldValue);
1830 e.getText().add(newValue);
1833 for (i = 0; i < oldValue.length() && i < newValue.length(); ++i) {
1834 if (oldValue.charAt(i) != newValue.charAt(i)) {
1838 if (i >= oldValue.length() && i >= newValue.length()) {
1841 int firstDifference = i;
1842 e.setFromIndex(firstDifference);
1844 int oldIndex = oldValue.length() - 1;
1845 int newIndex = newValue.length() - 1;
1846 while (oldIndex >= firstDifference && newIndex >= firstDifference) {
1847 if (oldValue.charAt(oldIndex) != newValue.charAt(newIndex)) {
1853 e.setRemovedCount(oldIndex - firstDifference + 1);
1854 e.setAddedCount(newIndex - firstDifference + 1);
1867 public void sendAccessibilityEvent(
int viewId,
int eventType) {
1868 if (!accessibilityManager.isEnabled()) {
1871 sendAccessibilityEvent(obtainAccessibilityEvent(viewId, eventType));
1881 private void sendAccessibilityEvent(@NonNull AccessibilityEvent
event) {
1882 if (!accessibilityManager.isEnabled()) {
1889 rootAccessibilityView.getParent().requestSendAccessibilityEvent(rootAccessibilityView,
event);
1903 private void onWindowNameChange(@NonNull SemanticsNode route) {
1904 String routeName =
route.getRouteName();
1905 if (routeName ==
null) {
1916 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
1917 setAccessibilityPaneTitle(routeName);
1919 AccessibilityEvent
event =
1920 obtainAccessibilityEvent(
route.id, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
1921 event.getText().add(routeName);
1922 sendAccessibilityEvent(
event);
1926 @TargetApi(API_LEVELS.API_28)
1927 @RequiresApi(API_LEVELS.API_28)
1928 private
void setAccessibilityPaneTitle(String title) {
1929 rootAccessibilityView.setAccessibilityPaneTitle(title);
1942 private void sendWindowContentChangeEvent(
int virtualViewId) {
1943 AccessibilityEvent
event =
1944 obtainAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
1945 event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
1946 sendAccessibilityEvent(
event);
1957 private AccessibilityEvent obtainAccessibilityEvent(
int virtualViewId,
int eventType) {
1958 AccessibilityEvent
event = obtainAccessibilityEvent(eventType);
1959 event.setPackageName(rootAccessibilityView.getContext().getPackageName());
1960 event.setSource(rootAccessibilityView, virtualViewId);
1965 public AccessibilityEvent obtainAccessibilityEvent(
int eventType) {
1966 return AccessibilityEvent.obtain(eventType);
1975 @TargetApi(API_LEVELS.API_28)
1976 @RequiresApi(API_LEVELS.API_28)
1977 private
boolean doesLayoutInDisplayCutoutModeRequireLeftInset() {
1978 Context context = rootAccessibilityView.getContext();
1979 Activity activity = ViewUtils.getActivity(context);
1980 if (activity ==
null || activity.getWindow() ==
null) {
1985 int layoutInDisplayCutoutMode = activity.getWindow().getAttributes().layoutInDisplayCutoutMode;
1986 return layoutInDisplayCutoutMode
1987 == WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
1988 || layoutInDisplayCutoutMode
1989 == WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
1996 private void willRemoveSemanticsNode(SemanticsNode semanticsNodeToBeRemoved) {
1997 if (BuildConfig.DEBUG) {
1998 if (!flutterSemanticsTree.containsKey(semanticsNodeToBeRemoved.id)) {
1999 Log.e(TAG,
"Attempted to remove a node that is not in the tree.");
2001 if (flutterSemanticsTree.get(semanticsNodeToBeRemoved.id) != semanticsNodeToBeRemoved) {
2002 Log.e(TAG,
"Flutter semantics tree failed to get expected node when searching by id.");
2009 semanticsNodeToBeRemoved.parent =
null;
2011 if (semanticsNodeToBeRemoved.platformViewId != -1
2012 && embeddedAccessibilityFocusedNodeId !=
null
2013 && accessibilityViewEmbedder.platformViewOfNode(embeddedAccessibilityFocusedNodeId)
2014 == platformViewsAccessibilityDelegate.getPlatformViewById(
2015 semanticsNodeToBeRemoved.platformViewId)) {
2018 sendAccessibilityEvent(
2019 embeddedAccessibilityFocusedNodeId,
2020 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2021 embeddedAccessibilityFocusedNodeId =
null;
2024 if (semanticsNodeToBeRemoved.platformViewId != -1) {
2026 platformViewsAccessibilityDelegate.getPlatformViewById(
2027 semanticsNodeToBeRemoved.platformViewId);
2028 if (embeddedView !=
null) {
2029 embeddedView.setImportantForAccessibility(
2030 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
2034 if (accessibilityFocusedSemanticsNode == semanticsNodeToBeRemoved) {
2035 sendAccessibilityEvent(
2036 accessibilityFocusedSemanticsNode.id,
2037 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2038 accessibilityFocusedSemanticsNode =
null;
2041 if (inputFocusedSemanticsNode == semanticsNodeToBeRemoved) {
2042 inputFocusedSemanticsNode =
null;
2045 if (hoveredObject == semanticsNodeToBeRemoved) {
2046 hoveredObject =
null;
2061 public void reset() {
2062 flutterSemanticsTree.clear();
2063 if (accessibilityFocusedSemanticsNode !=
null) {
2064 sendAccessibilityEvent(
2065 accessibilityFocusedSemanticsNode.id,
2066 AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
2068 accessibilityFocusedSemanticsNode =
null;
2069 hoveredObject =
null;
2070 sendWindowContentChangeEvent(0);
2077 public interface OnAccessibilityChangeListener {
2078 void onAccessibilityChanged(
boolean isAccessibilityEnabled,
boolean isTouchExplorationEnabled);
2086 SCROLL_LEFT(1 << 2),
2087 SCROLL_RIGHT(1 << 3),
2089 SCROLL_DOWN(1 << 5),
2092 SHOW_ON_SCREEN(1 << 8),
2093 MOVE_CURSOR_FORWARD_BY_CHARACTER(1 << 9),
2094 MOVE_CURSOR_BACKWARD_BY_CHARACTER(1 << 10),
2095 SET_SELECTION(1 << 11),
2099 DID_GAIN_ACCESSIBILITY_FOCUS(1 << 15),
2100 DID_LOSE_ACCESSIBILITY_FOCUS(1 << 16),
2101 CUSTOM_ACTION(1 << 17),
2103 MOVE_CURSOR_FORWARD_BY_WORD(1 << 19),
2104 MOVE_CURSOR_BACKWARD_BY_WORD(1 << 20),
2107 public final int value;
2117 static int systemAction =
2118 Action.DID_GAIN_ACCESSIBILITY_FOCUS.value
2119 & Action.DID_LOSE_ACCESSIBILITY_FOCUS.value
2120 &
Action.SHOW_ON_SCREEN.value;
2125 HAS_CHECKED_STATE(1 << 0),
2127 IS_SELECTED(1 << 2),
2129 IS_TEXT_FIELD(1 << 4),
2131 HAS_ENABLED_STATE(1 << 6),
2133 IS_IN_MUTUALLY_EXCLUSIVE_GROUP(1 << 8),
2135 IS_OBSCURED(1 << 10),
2136 SCOPES_ROUTE(1 << 11),
2137 NAMES_ROUTE(1 << 12),
2140 IS_LIVE_REGION(1 << 15),
2141 HAS_TOGGLED_STATE(1 << 16),
2142 IS_TOGGLED(1 << 17),
2143 HAS_IMPLICIT_SCROLLING(1 << 18),
2144 IS_MULTILINE(1 << 19),
2145 IS_READ_ONLY(1 << 20),
2146 IS_FOCUSABLE(1 << 21),
2149 IS_KEYBOARD_KEY(1 << 24),
2150 IS_CHECK_STATE_MIXED(1 << 25),
2151 HAS_EXPANDED_STATE(1 << 26),
2152 IS_EXPANDED(1 << 27);
2162 private enum AccessibilityFeature {
2163 ACCESSIBLE_NAVIGATION(1 << 0),
2164 INVERT_COLORS(1 << 1),
2165 DISABLE_ANIMATIONS(1 << 2),
2167 REDUCE_MOTION(1 << 4),
2168 HIGH_CONTRAST(1 << 5),
2169 ON_OFF_SWITCH_LABELS(1 << 6);
2173 AccessibilityFeature(
int value) {
2211 private static class CustomAccessibilityAction {
2212 CustomAccessibilityAction() {}
2217 private int resourceId = -1;
2222 private int id = -1;
2227 private int overrideId = -1;
2230 private String label;
2233 private String hint;
2253 private static class SpellOutStringAttribute
extends StringAttribute {}
2271 private static class SemanticsNode {
2272 private static boolean nullableHasAncestor(
2273 SemanticsNode
target, Predicate<SemanticsNode>
tester) {
2277 final AccessibilityBridge accessibilityBridge;
2280 private int id = -1;
2283 private int actions;
2284 private int maxValueLength;
2285 private int currentValueLength;
2286 private int textSelectionBase;
2287 private int textSelectionExtent;
2288 private int platformViewId;
2289 private int scrollChildren;
2290 private int scrollIndex;
2291 private float scrollPosition;
2292 private float scrollExtentMax;
2293 private float scrollExtentMin;
2295 private String label;
2297 private String
value;
2299 private String increasedValue;
2301 private String decreasedValue;
2303 private String hint;
2310 @Nullable
private String tooltip;
2323 private int previousNodeId = -1;
2328 private boolean hadPreviousConfig =
false;
2329 private int previousFlags;
2330 private int previousActions;
2331 private int previousTextSelectionBase;
2332 private int previousTextSelectionExtent;
2333 private float previousScrollPosition;
2334 private float previousScrollExtentMax;
2335 private float previousScrollExtentMin;
2336 private String previousValue;
2337 private String previousLabel;
2341 private float right;
2342 private float bottom;
2345 private SemanticsNode parent;
2349 private CustomAccessibilityAction onTapOverride;
2350 private CustomAccessibilityAction onLongPressOverride;
2352 private boolean inverseTransformDirty =
true;
2353 private float[] inverseTransform;
2355 private boolean globalGeometryDirty =
true;
2356 private float[] globalTransform;
2357 private Rect globalRect;
2359 SemanticsNode(@NonNull AccessibilityBridge accessibilityBridge) {
2360 this.accessibilityBridge = accessibilityBridge;
2367 private SemanticsNode getAncestor(Predicate<SemanticsNode>
tester) {
2368 SemanticsNode nextAncestor = parent;
2369 while (nextAncestor !=
null) {
2370 if (
tester.test(nextAncestor)) {
2371 return nextAncestor;
2373 nextAncestor = nextAncestor.parent;
2384 private boolean hasAction(@NonNull Action
action) {
2385 return (actions &
action.value) != 0;
2392 private boolean hadAction(@NonNull Action
action) {
2393 return (previousActions &
action.value) != 0;
2396 private boolean hasFlag(@NonNull Flag
flag) {
2400 private boolean hadFlag(@NonNull Flag
flag) {
2401 if (BuildConfig.DEBUG && !hadPreviousConfig) {
2402 Log.e(
TAG,
"Attempted to check hadFlag but had no previous config.");
2404 return (previousFlags &
flag.value) != 0;
2407 private boolean didScroll() {
2408 return !Float.isNaN(scrollPosition)
2409 && !Float.isNaN(previousScrollPosition)
2410 && previousScrollPosition != scrollPosition;
2413 private boolean didChangeLabel() {
2414 if (label ==
null && previousLabel ==
null) {
2417 return label ==
null || previousLabel ==
null || !label.equals(previousLabel);
2420 private void log(@NonNull String indent,
boolean recursive) {
2421 if (BuildConfig.DEBUG) {
2425 +
"SemanticsNode id="
2437 +
" +-- textDirection="
2441 +
" +-- rect.ltrb=("
2455 String childIndent = indent +
" ";
2456 for (SemanticsNode child : childrenInTraversalOrder) {
2457 child.log(childIndent, recursive);
2463 private void updateWith(
2464 @NonNull ByteBuffer
buffer,
2465 @NonNull String[] strings,
2466 @NonNull ByteBuffer[] stringAttributeArgs) {
2467 hadPreviousConfig =
true;
2468 previousValue =
value;
2469 previousLabel = label;
2470 previousFlags =
flags;
2471 previousActions = actions;
2472 previousTextSelectionBase = textSelectionBase;
2473 previousTextSelectionExtent = textSelectionExtent;
2474 previousScrollPosition = scrollPosition;
2475 previousScrollExtentMax = scrollExtentMax;
2476 previousScrollExtentMin = scrollExtentMin;
2479 actions =
buffer.getInt();
2480 maxValueLength =
buffer.getInt();
2481 currentValueLength =
buffer.getInt();
2482 textSelectionBase =
buffer.getInt();
2483 textSelectionExtent =
buffer.getInt();
2484 platformViewId =
buffer.getInt();
2485 scrollChildren =
buffer.getInt();
2486 scrollIndex =
buffer.getInt();
2487 scrollPosition =
buffer.getFloat();
2488 scrollExtentMax =
buffer.getFloat();
2489 scrollExtentMin =
buffer.getFloat();
2491 int stringIndex =
buffer.getInt();
2493 identifier = stringIndex == -1 ? null : strings[stringIndex];
2494 stringIndex =
buffer.getInt();
2496 label = stringIndex == -1 ? null : strings[stringIndex];
2498 labelAttributes = getStringAttributesFromBuffer(
buffer, stringAttributeArgs);
2500 stringIndex =
buffer.getInt();
2501 value = stringIndex == -1 ? null : strings[stringIndex];
2503 valueAttributes = getStringAttributesFromBuffer(
buffer, stringAttributeArgs);
2505 stringIndex =
buffer.getInt();
2506 increasedValue = stringIndex == -1 ? null : strings[stringIndex];
2508 increasedValueAttributes = getStringAttributesFromBuffer(
buffer, stringAttributeArgs);
2510 stringIndex =
buffer.getInt();
2511 decreasedValue = stringIndex == -1 ? null : strings[stringIndex];
2513 decreasedValueAttributes = getStringAttributesFromBuffer(
buffer, stringAttributeArgs);
2515 stringIndex =
buffer.getInt();
2516 hint = stringIndex == -1 ? null : strings[stringIndex];
2518 hintAttributes = getStringAttributesFromBuffer(
buffer, stringAttributeArgs);
2520 stringIndex =
buffer.getInt();
2521 tooltip = stringIndex == -1 ? null : strings[stringIndex];
2528 bottom =
buffer.getFloat();
2533 for (
int i = 0; i < 16; ++i) {
2536 inverseTransformDirty =
true;
2537 globalGeometryDirty =
true;
2539 final int childCount =
buffer.getInt();
2540 childrenInTraversalOrder.clear();
2541 childrenInHitTestOrder.clear();
2542 for (
int i = 0; i < childCount; ++i) {
2543 SemanticsNode child = accessibilityBridge.getOrCreateSemanticsNode(
buffer.getInt());
2544 child.parent =
this;
2545 childrenInTraversalOrder.
add(child);
2547 for (
int i = 0; i < childCount; ++i) {
2548 SemanticsNode child = accessibilityBridge.getOrCreateSemanticsNode(
buffer.getInt());
2549 child.parent =
this;
2550 childrenInHitTestOrder.
add(child);
2553 final int actionCount =
buffer.getInt();
2554 if (actionCount == 0) {
2555 customAccessibilityActions =
null;
2557 if (customAccessibilityActions ==
null)
2558 customAccessibilityActions =
new ArrayList<>(actionCount);
2559 else customAccessibilityActions.clear();
2561 for (
int i = 0; i < actionCount; i++) {
2562 CustomAccessibilityAction
action =
2563 accessibilityBridge.getOrCreateAccessibilityAction(
buffer.getInt());
2566 }
else if (
action.overrideId ==
Action.LONG_PRESS.value) {
2567 onLongPressOverride =
action;
2571 if (BuildConfig.DEBUG &&
action.overrideId != -1) {
2572 Log.e(
TAG,
"Expected action.overrideId to be -1.");
2582 @NonNull ByteBuffer
buffer, @NonNull ByteBuffer[] stringAttributeArgs) {
2583 final int attributesCount =
buffer.getInt();
2584 if (attributesCount == -1) {
2588 for (
int i = 0; i < attributesCount; ++i) {
2597 SpellOutStringAttribute attribute =
new SpellOutStringAttribute();
2598 attribute.start =
start;
2599 attribute.end =
end;
2600 attribute.type =
type;
2606 final int argsIndex =
buffer.getInt();
2607 final ByteBuffer
args = stringAttributeArgs[argsIndex];
2608 LocaleStringAttribute attribute =
new LocaleStringAttribute();
2609 attribute.start =
start;
2610 attribute.end =
end;
2611 attribute.type =
type;
2612 attribute.locale = Charset.forName(
"UTF-8").decode(
args).toString();
2623 private void ensureInverseTransform() {
2624 if (!inverseTransformDirty) {
2627 inverseTransformDirty =
false;
2628 if (inverseTransform ==
null) {
2629 inverseTransform =
new float[16];
2631 if (!Matrix.invertM(inverseTransform, 0,
transform, 0)) {
2632 Arrays.fill(inverseTransform, 0);
2636 private Rect getGlobalRect() {
2637 if (BuildConfig.DEBUG && globalGeometryDirty) {
2638 Log.e(
TAG,
"Attempted to getGlobalRect with a dirty geometry.");
2651 private SemanticsNode hitTest(
float[] point,
boolean stopAtPlatformView) {
2652 final float w = point[3];
2653 final float x = point[0] /
w;
2654 final float y = point[1] /
w;
2655 if (x < left || x >=
right || y < top || y >= bottom)
return null;
2656 final float[] transformedPoint =
new float[4];
2657 for (SemanticsNode child : childrenInHitTestOrder) {
2658 if (child.hasFlag(Flag.IS_HIDDEN)) {
2661 child.ensureInverseTransform();
2662 Matrix.multiplyMV(transformedPoint, 0, child.inverseTransform, 0, point, 0);
2663 final SemanticsNode
result = child.hitTest(transformedPoint, stopAtPlatformView);
2668 final boolean foundPlatformView = stopAtPlatformView && platformViewId != -1;
2669 return isFocusable() || foundPlatformView ? this :
null;
2674 private boolean isFocusable() {
2677 if (hasFlag(Flag.SCOPES_ROUTE)) {
2680 if (hasFlag(Flag.IS_FOCUSABLE)) {
2686 return (actions & ~SCROLLABLE_ACTIONS) != 0
2687 || (
flags & FOCUSABLE_FLAGS) != 0
2688 || (label !=
null && !label.isEmpty())
2689 || (value !=
null && !
value.isEmpty())
2690 || (hint !=
null && !hint.isEmpty());
2694 if (hasFlag(Flag.SCOPES_ROUTE)) {
2697 for (SemanticsNode child : childrenInTraversalOrder) {
2698 child.collectRoutes(edges);
2702 private String getRouteName() {
2705 if (hasFlag(Flag.NAMES_ROUTE)) {
2706 if (label !=
null && !label.isEmpty()) {
2710 for (SemanticsNode child : childrenInTraversalOrder) {
2711 String newName = child.getRouteName();
2712 if (newName !=
null && !newName.isEmpty()) {
2719 private void updateRecursively(
2720 float[] ancestorTransform, Set<SemanticsNode> visitedObjects,
boolean forceUpdate) {
2721 visitedObjects.add(
this);
2723 if (globalGeometryDirty) {
2728 if (globalTransform ==
null) {
2729 globalTransform =
new float[16];
2732 if (BuildConfig.DEBUG) {
2733 Log.e(
TAG,
"transform has not been initialized for id = " +
id);
2734 accessibilityBridge.getRootSemanticsNode().log(
"Semantics tree:",
true);
2738 Matrix.multiplyMM(globalTransform, 0, ancestorTransform, 0,
transform, 0);
2740 final float[] sample =
new float[4];
2744 final float[] point1 =
new float[4];
2745 final float[] point2 =
new float[4];
2746 final float[] point3 =
new float[4];
2747 final float[] point4 =
new float[4];
2751 transformPoint(point1, globalTransform, sample);
2755 transformPoint(point2, globalTransform, sample);
2759 transformPoint(point3, globalTransform, sample);
2763 transformPoint(point4, globalTransform, sample);
2765 if (globalRect ==
null) globalRect =
new Rect();
2768 Math.round(
min(point1[0], point2[0], point3[0], point4[0])),
2769 Math.round(
min(point1[1], point2[1], point3[1], point4[1])),
2770 Math.round(
max(point1[0], point2[0], point3[0], point4[0])),
2771 Math.round(
max(point1[1], point2[1], point3[1], point4[1])));
2773 globalGeometryDirty =
false;
2776 if (BuildConfig.DEBUG) {
2777 if (globalTransform ==
null) {
2778 Log.e(
TAG,
"Expected globalTransform to not be null.");
2780 if (globalRect ==
null) {
2781 Log.e(
TAG,
"Expected globalRect to not be null.");
2785 int previousNodeId = -1;
2786 for (SemanticsNode child : childrenInTraversalOrder) {
2787 child.previousNodeId = previousNodeId;
2788 previousNodeId = child.id;
2789 child.updateRecursively(globalTransform, visitedObjects, forceUpdate);
2793 private void transformPoint(
float[]
result,
float[]
transform,
float[] point) {
2802 private float min(
float a,
float b,
float c,
float d) {
2803 return Math.min(
a, Math.min(
b, Math.min(c,
d)));
2806 private float max(
float a,
float b,
float c,
float d) {
2807 return Math.max(
a, Math.max(
b, Math.max(c,
d)));
2810 private CharSequence getValue() {
2811 return createSpannableString(value, valueAttributes);
2814 private CharSequence getLabel() {
2815 return createSpannableString(label, labelAttributes);
2818 private CharSequence getHint() {
2819 return createSpannableString(hint, hintAttributes);
2822 private CharSequence getValueLabelHint() {
2823 CharSequence[] array =
new CharSequence[] {getValue(), getLabel(), getHint()};
2824 CharSequence
result =
null;
2825 for (CharSequence word : array) {
2826 if (word !=
null &&
word.length() > 0) {
2837 private CharSequence getTextFieldHint() {
2838 CharSequence[] array =
new CharSequence[] {getLabel(), getHint()};
2839 CharSequence
result =
null;
2840 for (CharSequence word : array) {
2841 if (word !=
null &&
word.length() > 0) {
2853 if (
string ==
null) {
2856 final SpannableString spannableString =
new SpannableString(
string);
2857 if (attributes !=
null) {
2858 for (StringAttribute attribute : attributes) {
2859 switch (attribute.type) {
2862 final TtsSpan ttsSpan =
new TtsSpan.Builder<>(TtsSpan.TYPE_VERBATIM).
build();
2863 spannableString.setSpan(ttsSpan, attribute.start, attribute.end, 0);
2868 LocaleStringAttribute localeAttribute = (LocaleStringAttribute) attribute;
2869 Locale locale = Locale.forLanguageTag(localeAttribute.locale);
2870 final LocaleSpan localeSpan =
new LocaleSpan(locale);
2871 spannableString.setSpan(localeSpan, attribute.start, attribute.end, 0);
2877 return spannableString;
2898 @SuppressLint(
"SwitchIntDef")
2899 public
boolean externalViewRequestSendAccessibilityEvent(
2900 View embeddedView, View eventOrigin, AccessibilityEvent
event) {
2901 if (!accessibilityViewEmbedder.requestSendAccessibilityEvent(
2902 embeddedView, eventOrigin,
event)) {
2905 Integer virtualNodeId = accessibilityViewEmbedder.getRecordFlutterId(embeddedView,
event);
2906 if (virtualNodeId ==
null) {
2909 switch (
event.getEventType()) {
2910 case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
2911 hoveredObject =
null;
2913 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
2914 embeddedAccessibilityFocusedNodeId = virtualNodeId;
2915 accessibilityFocusedSemanticsNode =
null;
2917 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
2918 embeddedInputFocusedNodeId =
null;
2919 embeddedAccessibilityFocusedNodeId =
null;
2921 case AccessibilityEvent.TYPE_VIEW_FOCUSED:
2922 embeddedInputFocusedNodeId = virtualNodeId;
2923 inputFocusedSemanticsNode =
null;