5package io.flutter.view;
7import static io.flutter.Build.API_LEVELS;
9import android.annotation.SuppressLint;
17import android.view.accessibility.AccessibilityEvent;
18import android.view.accessibility.AccessibilityNodeInfo;
19import android.view.accessibility.AccessibilityNodeProvider;
20import android.view.accessibility.AccessibilityRecord;
21import androidx.annotation.Keep;
22import androidx.annotation.NonNull;
23import androidx.annotation.Nullable;
25import java.lang.reflect.Field;
26import java.lang.reflect.InvocationTargetException;
27import java.lang.reflect.Method;
28import java.util.HashMap;
50 private static final String TAG =
"AccessibilityBridge";
52 private final ReflectionAccessors reflectionAccessors;
55 private final View rootAccessibilityView;
58 private final SparseArray<ViewAndId> flutterIdToOrigin;
61 private final Map<ViewAndId, Integer> originToFlutterId;
67 private final Map<View, Rect> embeddedViewToDisplayBounds;
69 private int nextFlutterId;
72 reflectionAccessors =
new ReflectionAccessors();
73 flutterIdToOrigin =
new SparseArray<>();
74 this.rootAccessibilityView = rootAccessibiiltyView;
75 nextFlutterId = firstVirtualNodeId;
76 originToFlutterId =
new HashMap<>();
77 embeddedViewToDisplayBounds =
new HashMap<>();
87 @NonNull View embeddedView,
int flutterId, @NonNull
Rect displayBounds) {
88 AccessibilityNodeInfo originNode = embeddedView.createAccessibilityNodeInfo();
89 Long originPackedId = reflectionAccessors.getSourceNodeId(originNode);
90 if (originPackedId ==
null) {
93 embeddedViewToDisplayBounds.put(embeddedView, displayBounds);
94 int originId = ReflectionAccessors.getVirtualNodeId(originPackedId);
95 cacheVirtualIdMappings(embeddedView, originId, flutterId);
96 return convertToFlutterNode(originNode, flutterId, embeddedView);
102 ViewAndId origin = flutterIdToOrigin.get(flutterId);
103 if (origin ==
null) {
106 if (!embeddedViewToDisplayBounds.containsKey(origin.view)) {
115 AccessibilityNodeProvider provider = origin.view.getAccessibilityNodeProvider();
116 if (provider ==
null) {
123 AccessibilityNodeInfo originNode =
124 origin.view.getAccessibilityNodeProvider().createAccessibilityNodeInfo(origin.id);
125 if (originNode ==
null) {
128 return convertToFlutterNode(originNode, flutterId, origin.view);
136 private AccessibilityNodeInfo convertToFlutterNode(
137 @NonNull AccessibilityNodeInfo originNode,
int flutterId, @NonNull View embeddedView) {
138 AccessibilityNodeInfo
result = AccessibilityNodeInfo.obtain(rootAccessibilityView, flutterId);
139 result.setPackageName(rootAccessibilityView.getContext().getPackageName());
140 result.setSource(rootAccessibilityView, flutterId);
141 result.setClassName(originNode.getClassName());
143 Rect displayBounds = embeddedViewToDisplayBounds.get(embeddedView);
145 copyAccessibilityFields(originNode,
result);
146 setFlutterNodesTranslateBounds(originNode, displayBounds,
result);
147 addChildrenToFlutterNode(originNode, embeddedView,
result);
148 setFlutterNodeParent(originNode, embeddedView,
result);
153 private void setFlutterNodeParent(
154 @NonNull AccessibilityNodeInfo originNode,
155 @NonNull View embeddedView,
156 @NonNull AccessibilityNodeInfo
result) {
157 Long parentOriginPackedId = reflectionAccessors.getParentNodeId(originNode);
158 if (parentOriginPackedId ==
null) {
161 int parentOriginId = ReflectionAccessors.getVirtualNodeId(parentOriginPackedId);
162 Integer parentFlutterId = originToFlutterId.get(
new ViewAndId(embeddedView, parentOriginId));
163 if (parentFlutterId !=
null) {
164 result.setParent(rootAccessibilityView, parentFlutterId);
168 private void addChildrenToFlutterNode(
169 @NonNull AccessibilityNodeInfo originNode,
170 @NonNull View embeddedView,
171 @NonNull AccessibilityNodeInfo resultNode) {
172 for (
int i = 0;
i < originNode.getChildCount();
i++) {
173 Long originPackedId = reflectionAccessors.getChildId(originNode,
i);
174 if (originPackedId ==
null) {
177 int originId = ReflectionAccessors.getVirtualNodeId(originPackedId);
178 ViewAndId origin =
new ViewAndId(embeddedView, originId);
180 if (originToFlutterId.containsKey(origin)) {
181 childFlutterId = originToFlutterId.get(origin);
183 childFlutterId = nextFlutterId++;
184 cacheVirtualIdMappings(embeddedView, originId, childFlutterId);
186 resultNode.addChild(rootAccessibilityView, childFlutterId);
193 private void cacheVirtualIdMappings(@NonNull View embeddedView,
int originId,
int flutterId) {
194 ViewAndId origin =
new ViewAndId(embeddedView, originId);
195 originToFlutterId.put(origin, flutterId);
196 flutterIdToOrigin.put(flutterId, origin);
202 @SuppressWarnings(
"deprecation")
203 private
void setFlutterNodesTranslateBounds(
204 @NonNull AccessibilityNodeInfo originNode,
205 @NonNull
Rect displayBounds,
206 @NonNull AccessibilityNodeInfo resultNode) {
208 originNode.getBoundsInParent(boundsInParent);
209 resultNode.setBoundsInParent(boundsInParent);
212 originNode.getBoundsInScreen(boundsInScreen);
213 boundsInScreen.offset(displayBounds.left, displayBounds.top);
214 resultNode.setBoundsInScreen(boundsInScreen);
217 private void copyAccessibilityFields(
218 @NonNull AccessibilityNodeInfo input, @NonNull AccessibilityNodeInfo
output) {
219 output.setAccessibilityFocused(input.isAccessibilityFocused());
220 output.setCheckable(input.isCheckable());
221 output.setChecked(input.isChecked());
222 output.setContentDescription(input.getContentDescription());
223 output.setEnabled(input.isEnabled());
224 output.setClickable(input.isClickable());
225 output.setFocusable(input.isFocusable());
226 output.setFocused(input.isFocused());
227 output.setLongClickable(input.isLongClickable());
228 output.setMovementGranularities(input.getMovementGranularities());
229 output.setPassword(input.isPassword());
230 output.setScrollable(input.isScrollable());
231 output.setSelected(input.isSelected());
232 output.setText(input.getText());
233 output.setVisibleToUser(input.isVisibleToUser());
235 output.setEditable(input.isEditable());
236 output.setCanOpenPopup(input.canOpenPopup());
237 output.setCollectionInfo(input.getCollectionInfo());
238 output.setCollectionItemInfo(input.getCollectionItemInfo());
239 output.setContentInvalid(input.isContentInvalid());
240 output.setDismissable(input.isDismissable());
241 output.setInputType(input.getInputType());
242 output.setLiveRegion(input.getLiveRegion());
243 output.setMultiLine(input.isMultiLine());
244 output.setRangeInfo(input.getRangeInfo());
245 output.setError(input.getError());
246 output.setMaxTextLength(input.getMaxTextLength());
247 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_23) {
248 output.setContextClickable(input.isContextClickable());
252 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_24) {
253 output.setDrawingOrder(input.getDrawingOrder());
254 output.setImportantForAccessibility(input.isImportantForAccessibility());
256 if (
Build.VERSION.SDK_INT >= API_LEVELS.API_26) {
257 output.setAvailableExtraData(input.getAvailableExtraData());
258 output.setHintText(input.getHintText());
259 output.setShowingHintText(input.isShowingHintText());
270 @NonNull View embeddedView, @NonNull View eventOrigin, @NonNull AccessibilityEvent
event) {
271 AccessibilityEvent translatedEvent = AccessibilityEvent.obtain(
event);
272 Long originPackedId = reflectionAccessors.getRecordSourceNodeId(
event);
273 if (originPackedId ==
null) {
276 int originVirtualId = ReflectionAccessors.getVirtualNodeId(originPackedId);
277 Integer flutterId = originToFlutterId.get(
new ViewAndId(embeddedView, originVirtualId));
278 if (flutterId ==
null) {
279 flutterId = nextFlutterId++;
280 cacheVirtualIdMappings(embeddedView, originVirtualId, flutterId);
282 translatedEvent.setSource(rootAccessibilityView, flutterId);
283 translatedEvent.setClassName(
event.getClassName());
284 translatedEvent.setPackageName(
event.getPackageName());
286 for (
int i = 0;
i < translatedEvent.getRecordCount();
i++) {
287 AccessibilityRecord record = translatedEvent.getRecord(
i);
288 Long recordOriginPackedId = reflectionAccessors.getRecordSourceNodeId(record);
289 if (recordOriginPackedId ==
null) {
292 int recordOriginVirtualID = ReflectionAccessors.getVirtualNodeId(recordOriginPackedId);
293 ViewAndId originViewAndId =
new ViewAndId(embeddedView, recordOriginVirtualID);
294 if (!originToFlutterId.containsKey(originViewAndId)) {
297 int recordFlutterId = originToFlutterId.get(originViewAndId);
298 record.setSource(rootAccessibilityView, recordFlutterId);
301 return rootAccessibilityView
303 .requestSendAccessibilityEvent(eventOrigin, translatedEvent);
312 public boolean performAction(
int flutterId,
int accessibilityAction, @Nullable Bundle arguments) {
313 ViewAndId origin = flutterIdToOrigin.get(flutterId);
314 if (origin ==
null) {
317 View embeddedView = origin.view;
318 AccessibilityNodeProvider provider = embeddedView.getAccessibilityNodeProvider();
319 if (provider ==
null) {
322 return provider.performAction(origin.id, accessibilityAction, arguments);
332 @NonNull View embeddedView, @NonNull AccessibilityRecord record) {
333 Long originPackedId = reflectionAccessors.getRecordSourceNodeId(record);
334 if (originPackedId ==
null) {
337 int originVirtualId = ReflectionAccessors.getVirtualNodeId(originPackedId);
338 return originToFlutterId.get(
new ViewAndId(embeddedView, originVirtualId));
347 ViewAndId origin = flutterIdToOrigin.get(rootFlutterId);
348 if (origin ==
null) {
351 Rect displayBounds = embeddedViewToDisplayBounds.get(origin.view);
352 int pointerCount =
event.getPointerCount();
353 MotionEvent.PointerProperties[] pointerProperties =
354 new MotionEvent.PointerProperties[pointerCount];
355 MotionEvent.PointerCoords[] pointerCoords =
new MotionEvent.PointerCoords[pointerCount];
356 for (
int i = 0;
i <
event.getPointerCount();
i++) {
357 pointerProperties[
i] =
new MotionEvent.PointerProperties();
358 event.getPointerProperties(
i, pointerProperties[
i]);
360 MotionEvent.PointerCoords originCoords =
new MotionEvent.PointerCoords();
361 event.getPointerCoords(
i, originCoords);
363 pointerCoords[
i] =
new MotionEvent.PointerCoords(originCoords);
364 pointerCoords[
i].x -= displayBounds.left;
365 pointerCoords[
i].y -= displayBounds.top;
367 MotionEvent translatedEvent =
370 event.getEventTime(),
372 event.getPointerCount(),
375 event.getMetaState(),
376 event.getButtonState(),
377 event.getXPrecision(),
378 event.getYPrecision(),
380 event.getEdgeFlags(),
383 return origin.view.dispatchGenericMotionEvent(translatedEvent);
391 ViewAndId viewAndId = flutterIdToOrigin.get(flutterId);
392 if (viewAndId ==
null) {
395 return viewAndId.view;
398 private static class ViewAndId {
402 private ViewAndId(View view,
int id) {
408 public boolean equals(Object o) {
409 if (
this == o)
return true;
410 if (!(o instanceof ViewAndId))
return false;
411 ViewAndId viewAndId = (ViewAndId) o;
412 return id == viewAndId.id && view.equals(viewAndId.view);
416 public int hashCode() {
417 final int prime = 31;
425 private static class ReflectionAccessors {
426 private @Nullable
final Method getSourceNodeId;
427 private @Nullable
final Method getParentNodeId;
428 private @Nullable
final Method getRecordSourceNodeId;
429 private @Nullable
final Method getChildId;
430 private @Nullable
final Field childNodeIdsField;
431 private @Nullable
final Method longArrayGetIndex;
433 @SuppressLint(
"DiscouragedPrivateApi,PrivateApi")
434 private ReflectionAccessors() {
435 Method getSourceNodeId =
null;
436 Method getParentNodeId =
null;
437 Method getRecordSourceNodeId =
null;
438 Method getChildId =
null;
439 Field childNodeIdsField =
null;
440 Method longArrayGetIndex =
null;
442 getSourceNodeId = AccessibilityNodeInfo.class.getMethod(
"getSourceNodeId");
443 }
catch (NoSuchMethodException
e) {
444 Log.w(TAG,
"can't invoke AccessibilityNodeInfo#getSourceNodeId with reflection");
447 getRecordSourceNodeId = AccessibilityRecord.class.getMethod(
"getSourceNodeId");
448 }
catch (NoSuchMethodException
e) {
449 Log.w(TAG,
"can't invoke AccessibiiltyRecord#getSourceNodeId with reflection");
452 if (
Build.VERSION.SDK_INT <= API_LEVELS.API_26) {
454 getParentNodeId = AccessibilityNodeInfo.class.getMethod(
"getParentNodeId");
455 }
catch (NoSuchMethodException
e) {
456 Log.w(TAG,
"can't invoke getParentNodeId with reflection");
461 getChildId = AccessibilityNodeInfo.class.getMethod(
"getChildId",
int.
class);
462 }
catch (NoSuchMethodException
e) {
463 Log.w(TAG,
"can't invoke getChildId with reflection");
467 childNodeIdsField = AccessibilityNodeInfo.class.getDeclaredField(
"mChildNodeIds");
468 childNodeIdsField.setAccessible(
true);
471 longArrayGetIndex = Class.forName(
"android.util.LongArray").getMethod(
"get",
int.
class);
472 }
catch (NoSuchFieldException
473 | ClassNotFoundException
474 | NoSuchMethodException
475 | NullPointerException
e) {
476 Log.w(TAG,
"can't access childNodeIdsField with reflection");
477 childNodeIdsField =
null;
480 this.getSourceNodeId = getSourceNodeId;
481 this.getParentNodeId = getParentNodeId;
482 this.getRecordSourceNodeId = getRecordSourceNodeId;
483 this.getChildId = getChildId;
484 this.childNodeIdsField = childNodeIdsField;
485 this.longArrayGetIndex = longArrayGetIndex;
489 private static int getVirtualNodeId(
long nodeId) {
490 return (
int) (nodeId >> 32);
494 private Long getSourceNodeId(@NonNull AccessibilityNodeInfo node) {
495 if (getSourceNodeId ==
null) {
499 return (Long) getSourceNodeId.invoke(node);
500 }
catch (IllegalAccessException
e) {
501 Log.w(TAG,
"Failed to access getSourceNodeId method.",
e);
502 }
catch (InvocationTargetException
e) {
503 Log.w(TAG,
"The getSourceNodeId method threw an exception when invoked.",
e);
509 private Long getChildId(@NonNull AccessibilityNodeInfo node,
int child) {
510 if (getChildId ==
null && (childNodeIdsField ==
null || longArrayGetIndex ==
null)) {
513 if (getChildId !=
null) {
515 return (Long) getChildId.invoke(node, child);
521 }
catch (IllegalAccessException
e) {
522 Log.w(TAG,
"Failed to access getChildId method.",
e);
523 }
catch (InvocationTargetException
e) {
524 Log.w(TAG,
"The getChildId method threw an exception when invoked.",
e);
528 return (
long) longArrayGetIndex.invoke(childNodeIdsField.get(node), child);
534 }
catch (IllegalAccessException
e) {
535 Log.w(TAG,
"Failed to access longArrayGetIndex method or the childNodeId field.",
e);
536 }
catch (InvocationTargetException | ArrayIndexOutOfBoundsException
e) {
537 Log.w(TAG,
"The longArrayGetIndex method threw an exception when invoked.",
e);
544 private Long getParentNodeId(@NonNull AccessibilityNodeInfo node) {
545 if (getParentNodeId !=
null) {
547 return (
long) getParentNodeId.invoke(node);
553 }
catch (IllegalAccessException
e) {
554 Log.w(TAG,
"Failed to access getParentNodeId method.",
e);
555 }
catch (InvocationTargetException
e) {
556 Log.w(TAG,
"The getParentNodeId method threw an exception when invoked.",
e);
561 return yoinkParentIdFromParcel(node);
569 private static Long yoinkParentIdFromParcel(AccessibilityNodeInfo node) {
570 if (
Build.VERSION.SDK_INT < API_LEVELS.API_26) {
571 Log.w(TAG,
"Unexpected Android version. Unable to find the parent ID.");
579 AccessibilityNodeInfo
copy = AccessibilityNodeInfo.obtain(node);
580 final Parcel parcel = Parcel.obtain();
581 parcel.setDataPosition(0);
582 copy.writeToParcel(parcel, 0);
583 Long parentNodeId =
null;
587 parcel.setDataPosition(0);
588 long nonDefaultFields = parcel.readLong();
590 if (isBitSet(nonDefaultFields, fieldIndex++)) {
593 if (isBitSet(nonDefaultFields, fieldIndex++)) {
596 if (isBitSet(nonDefaultFields, fieldIndex++)) {
599 if (isBitSet(nonDefaultFields, fieldIndex++)) {
600 parentNodeId = parcel.readLong();
607 private static boolean isBitSet(
long flags,
int bitIndex) {
608 return (
flags & (1
L << bitIndex)) != 0;
612 private Long getRecordSourceNodeId(@NonNull AccessibilityRecord node) {
613 if (getRecordSourceNodeId ==
null) {
617 return (Long) getRecordSourceNodeId.invoke(node);
618 }
catch (IllegalAccessException
e) {
619 Log.w(TAG,
"Failed to access the getRecordSourceNodeId method.",
e);
620 }
catch (InvocationTargetException
e) {
621 Log.w(TAG,
"The getRecordSourceNodeId method threw an exception when invoked.",
e);
bool equals(SkDrawable *a, SkDrawable *b)
AccessibilityViewEmbedder(@NonNull View rootAccessibiiltyView, int firstVirtualNodeId)
boolean onAccessibilityHoverEvent(int rootFlutterId, @NonNull MotionEvent event)
Integer getRecordFlutterId( @NonNull View embeddedView, @NonNull AccessibilityRecord record)
AccessibilityNodeInfo getRootNode( @NonNull View embeddedView, int flutterId, @NonNull Rect displayBounds)
boolean performAction(int flutterId, int accessibilityAction, @Nullable Bundle arguments)
AccessibilityNodeInfo createAccessibilityNodeInfo(int flutterId)
View platformViewOfNode(int flutterId)
boolean requestSendAccessibilityEvent( @NonNull View embeddedView, @NonNull View eventOrigin, @NonNull AccessibilityEvent event)
FlutterSemanticsFlag flags
void Log(const char *format,...) SK_PRINTF_LIKE(1
def Build(configs, env, options)