• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package io.flutter.view;
6 
7 import android.annotation.SuppressLint;
8 import android.graphics.Rect;
9 import android.os.Build;
10 import android.os.Bundle;
11 import android.os.Parcel;
12 import android.os.Parcelable;
13 import android.support.annotation.NonNull;
14 import android.support.annotation.Nullable;
15 import android.util.Log;
16 import android.util.SparseArray;
17 import android.view.MotionEvent;
18 import android.view.View;
19 import android.view.accessibility.AccessibilityEvent;
20 import android.view.accessibility.AccessibilityNodeInfo;
21 import android.view.accessibility.AccessibilityNodeProvider;
22 import android.view.accessibility.AccessibilityRecord;
23 
24 import java.lang.reflect.InvocationTargetException;
25 import java.lang.reflect.Field;
26 import java.lang.reflect.Method;
27 import java.util.HashMap;
28 import java.util.Map;
29 
30 /**
31  * Facilitates embedding of platform views in the accessibility tree generated by the accessibility bridge.
32  *
33  * Embedding is done by mirroring the accessibility tree of the platform view as a subtree of the flutter
34  * accessibility tree.
35  *
36  * This class relies on hidden system APIs to extract the accessibility information and does not work starting
37  * Android P; If the reflection accessors are not available we fail silently by embedding a null node, the app
38  * continues working but the accessibility information for the platform view will not be embedded.
39  *
40  * We use the term `flutterId` for virtual accessibility node IDs in the FlutterView tree, and the term `originId`
41  * for the virtual accessibility node IDs in the platform view's tree. Internally this class maintains a bidirectional
42  * mapping between `flutterId`s and the corresponding platform view and `originId`.
43  */
44 final class AccessibilityViewEmbedder {
45     private static final String TAG = "AccessibilityBridge";
46 
47     private final ReflectionAccessors reflectionAccessors;
48 
49     // The view to which the platform view is embedded, this is typically FlutterView.
50     private final View rootAccessibilityView;
51 
52     // Maps a flutterId to the corresponding platform view and originId.
53     private final SparseArray<ViewAndId> flutterIdToOrigin;
54 
55     // Maps a platform view and originId to a corresponding flutterID.
56     private final Map<ViewAndId, Integer> originToFlutterId;
57 
58     // Maps an embedded view to it's screen bounds.
59     // This is used to translate the coordinates of the accessibility node subtree to the main display's coordinate
60     // system.
61     private final Map<View, Rect> embeddedViewToDisplayBounds;
62 
63     private int nextFlutterId;
64 
AccessibilityViewEmbedder(@onNull View rootAccessibiiltyView, int firstVirtualNodeId)65     AccessibilityViewEmbedder(@NonNull View rootAccessibiiltyView, int firstVirtualNodeId) {
66         reflectionAccessors = new ReflectionAccessors();
67         flutterIdToOrigin = new SparseArray<>();
68         this.rootAccessibilityView = rootAccessibiiltyView;
69         nextFlutterId = firstVirtualNodeId;
70         originToFlutterId = new HashMap<>();
71         embeddedViewToDisplayBounds = new HashMap<>();
72     }
73 
74     /**
75      * Returns the root accessibility node for an embedded platform view.
76      *
77      * @param flutterId the virtual accessibility ID for the node in flutter accessibility tree
78      * @param displayBounds the display bounds for the node in screen coordinates
79      */
getRootNode(@onNull View embeddedView, int flutterId, @NonNull Rect displayBounds)80     public AccessibilityNodeInfo getRootNode(@NonNull View embeddedView, int flutterId, @NonNull Rect displayBounds) {
81         AccessibilityNodeInfo originNode = embeddedView.createAccessibilityNodeInfo();
82         Long originPackedId = reflectionAccessors.getSourceNodeId(originNode);
83         if (originPackedId == null) {
84             return null;
85         }
86         embeddedViewToDisplayBounds.put(embeddedView, displayBounds);
87         int originId = ReflectionAccessors.getVirtualNodeId(originPackedId);
88         cacheVirtualIdMappings(embeddedView, originId, flutterId);
89         return convertToFlutterNode(originNode, flutterId, embeddedView);
90     }
91 
92     /**
93      * Creates the accessibility node info for the node identified with `flutterId`.
94      */
95     @Nullable
createAccessibilityNodeInfo(int flutterId)96     public AccessibilityNodeInfo createAccessibilityNodeInfo(int flutterId) {
97         ViewAndId origin = flutterIdToOrigin.get(flutterId);
98         if (origin == null) {
99             return null;
100         }
101         if (!embeddedViewToDisplayBounds.containsKey(origin.view)) {
102             // This might happen if the embedded view is sending accessibility event before the first Flutter semantics
103             // tree was sent to the accessibility bridge. In this case we don't return a node as we do not know the
104             // bounds yet.
105             // https://github.com/flutter/flutter/issues/30068
106             return null;
107         }
108         AccessibilityNodeProvider provider = origin.view.getAccessibilityNodeProvider();
109         if (provider == null) {
110             // The provider is null for views that don't have a virtual accessibility tree.
111             // We currently only support embedding virtual hierarchies in the Flutter tree.
112             // TODO(amirh): support embedding non virtual hierarchies.
113             // https://github.com/flutter/flutter/issues/29717
114             return null;
115         }
116         AccessibilityNodeInfo originNode =
117                 origin.view.getAccessibilityNodeProvider().createAccessibilityNodeInfo(origin.id);
118         if (originNode == null) {
119             return null;
120         }
121         return convertToFlutterNode(originNode, flutterId, origin.view);
122     }
123 
124     /*
125      * Creates an AccessibilityNodeInfo that can be attached to the Flutter accessibility tree and is equivalent to
126      * originNode(which belongs to embeddedView). The virtual ID for the created node will be flutterId.
127      */
128     @NonNull
convertToFlutterNode( @onNull AccessibilityNodeInfo originNode, int flutterId, @NonNull View embeddedView )129     private AccessibilityNodeInfo convertToFlutterNode(
130             @NonNull AccessibilityNodeInfo originNode,
131             int flutterId,
132             @NonNull View embeddedView
133     ) {
134         AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(rootAccessibilityView, flutterId);
135         result.setPackageName(rootAccessibilityView.getContext().getPackageName());
136         result.setSource(rootAccessibilityView, flutterId);
137         result.setClassName(originNode.getClassName());
138 
139         Rect displayBounds = embeddedViewToDisplayBounds.get(embeddedView);
140 
141         copyAccessibilityFields(originNode, result);
142         setFlutterNodesTranslateBounds(originNode, displayBounds, result);
143         addChildrenToFlutterNode(originNode, embeddedView, result);
144         setFlutterNodeParent(originNode, embeddedView, result);
145 
146         return result;
147     }
148 
setFlutterNodeParent( @onNull AccessibilityNodeInfo originNode, @NonNull View embeddedView, @NonNull AccessibilityNodeInfo result )149     private void setFlutterNodeParent(
150             @NonNull AccessibilityNodeInfo originNode,
151             @NonNull View embeddedView,
152             @NonNull AccessibilityNodeInfo result
153     ) {
154         Long parentOriginPackedId = reflectionAccessors.getParentNodeId(originNode);
155         if (parentOriginPackedId == null) {
156             return;
157         }
158         int parentOriginId = ReflectionAccessors.getVirtualNodeId(parentOriginPackedId);
159         Integer parentFlutterId = originToFlutterId.get(new ViewAndId(embeddedView, parentOriginId));
160         if (parentFlutterId != null) {
161             result.setParent(rootAccessibilityView, parentFlutterId);
162         }
163     }
164 
165 
addChildrenToFlutterNode( @onNull AccessibilityNodeInfo originNode, @NonNull View embeddedView, @NonNull AccessibilityNodeInfo resultNode )166     private void addChildrenToFlutterNode(
167             @NonNull AccessibilityNodeInfo originNode,
168             @NonNull View embeddedView,
169             @NonNull AccessibilityNodeInfo resultNode
170     ) {
171         for (int i = 0; i < originNode.getChildCount(); i++) {
172             Long originPackedId = reflectionAccessors.getChildId(originNode, i);
173             if (originPackedId == null) {
174                 continue;
175             }
176             int originId = ReflectionAccessors.getVirtualNodeId(originPackedId);
177             ViewAndId origin = new ViewAndId(embeddedView, originId);
178             int childFlutterId;
179             if (originToFlutterId.containsKey(origin)) {
180                 childFlutterId = originToFlutterId.get(origin);
181             } else {
182                 childFlutterId = nextFlutterId++;
183                 cacheVirtualIdMappings(embeddedView, originId, childFlutterId);
184             }
185             resultNode.addChild(rootAccessibilityView, childFlutterId);
186         }
187     }
188 
189     // Caches a bidirectional mapping of (embeddedView, originId)<-->flutterId.
190     // Where originId is a virtual node ID in the embeddedView's tree, and flutterId is the ID
191     // of the corresponding node in the Flutter virtual accessibility nodes tree.
cacheVirtualIdMappings(@onNull View embeddedView, int originId, int flutterId)192     private void cacheVirtualIdMappings(@NonNull View embeddedView, int originId, int flutterId) {
193         ViewAndId origin = new ViewAndId(embeddedView, originId);
194         originToFlutterId.put(origin, flutterId);
195         flutterIdToOrigin.put(flutterId, origin);
196     }
197 
setFlutterNodesTranslateBounds( @onNull AccessibilityNodeInfo originNode, @NonNull Rect displayBounds, @NonNull AccessibilityNodeInfo resultNode )198     private void setFlutterNodesTranslateBounds(
199             @NonNull AccessibilityNodeInfo originNode,
200             @NonNull Rect displayBounds,
201             @NonNull AccessibilityNodeInfo resultNode
202     ) {
203         Rect boundsInScreen = new Rect();
204         originNode.getBoundsInScreen(boundsInScreen);
205         boundsInScreen.offset(displayBounds.left, displayBounds.top);
206         resultNode.setBoundsInScreen(boundsInScreen);
207     }
208 
copyAccessibilityFields(@onNull AccessibilityNodeInfo input, @NonNull AccessibilityNodeInfo output)209     private void copyAccessibilityFields(@NonNull AccessibilityNodeInfo input, @NonNull AccessibilityNodeInfo output) {
210         output.setAccessibilityFocused(input.isAccessibilityFocused());
211         output.setCheckable(input.isCheckable());
212         output.setChecked(input.isChecked());
213         output.setContentDescription(input.getContentDescription());
214         output.setEnabled(input.isEnabled());
215         output.setClickable(input.isClickable());
216         output.setFocusable(input.isFocusable());
217         output.setFocused(input.isFocused());
218         output.setLongClickable(input.isLongClickable());
219         output.setMovementGranularities(input.getMovementGranularities());
220         output.setPassword(input.isPassword());
221         output.setScrollable(input.isScrollable());
222         output.setSelected(input.isSelected());
223         output.setText(input.getText());
224         output.setVisibleToUser(input.isVisibleToUser());
225 
226         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
227             output.setEditable(input.isEditable());
228         }
229         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
230             output.setCanOpenPopup(input.canOpenPopup());
231             output.setCollectionInfo(input.getCollectionInfo());
232             output.setCollectionItemInfo(input.getCollectionItemInfo());
233             output.setContentInvalid(input.isContentInvalid());
234             output.setDismissable(input.isDismissable());
235             output.setInputType(input.getInputType());
236             output.setLiveRegion(input.getLiveRegion());
237             output.setMultiLine(input.isMultiLine());
238             output.setRangeInfo(input.getRangeInfo());
239         }
240         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
241             output.setError(input.getError());
242             output.setMaxTextLength(input.getMaxTextLength());
243         }
244         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
245             output.setContextClickable(input.isContextClickable());
246             // TODO(amirh): copy traversal before and after.
247             // https://github.com/flutter/flutter/issues/29718
248         }
249         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
250             output.setDrawingOrder(input.getDrawingOrder());
251             output.setImportantForAccessibility(input.isImportantForAccessibility());
252         }
253         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
254             output.setAvailableExtraData(input.getAvailableExtraData());
255             output.setHintText(input.getHintText());
256             output.setShowingHintText(input.isShowingHintText());
257         }
258     }
259 
260     /**
261      * Delegates an AccessibilityNodeProvider#requestSendAccessibilityEvent from the AccessibilityBridge to the embedded
262      * view.
263      *
264      * @return True if the event was sent.
265      */
requestSendAccessibilityEvent( @onNull View embeddedView, @NonNull View eventOrigin, @NonNull AccessibilityEvent event )266     public boolean requestSendAccessibilityEvent(
267             @NonNull View embeddedView,
268             @NonNull View eventOrigin,
269             @NonNull AccessibilityEvent event
270     ) {
271         AccessibilityEvent translatedEvent = AccessibilityEvent.obtain(event);
272         Long originPackedId = reflectionAccessors.getRecordSourceNodeId(event);
273         if (originPackedId == null) {
274             return false;
275         }
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);
281         }
282         translatedEvent.setSource(rootAccessibilityView, flutterId);
283         translatedEvent.setClassName(event.getClassName());
284         translatedEvent.setPackageName(event.getPackageName());
285 
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) {
290                 return false;
291             }
292             int recordOriginVirtualID = ReflectionAccessors.getVirtualNodeId(recordOriginPackedId);
293             ViewAndId originViewAndId = new ViewAndId(embeddedView, recordOriginVirtualID);
294             if (!originToFlutterId.containsKey(originViewAndId)) {
295                 return false;
296             }
297             int recordFlutterId = originToFlutterId.get(originViewAndId);
298             record.setSource(rootAccessibilityView, recordFlutterId);
299         }
300 
301         return rootAccessibilityView.getParent().requestSendAccessibilityEvent(eventOrigin, translatedEvent);
302     }
303 
304     /**
305      * Delegates an @{link AccessibilityNodeProvider#performAction} from the AccessibilityBridge to the embedded view's
306      * accessibility node provider.
307      *
308      * @return True if the action was performed.
309      */
performAction(int flutterId, int accessibilityAction, @Nullable Bundle arguments)310     public boolean performAction(int flutterId, int accessibilityAction, @Nullable Bundle arguments) {
311         ViewAndId origin  = flutterIdToOrigin.get(flutterId);
312         if (origin == null) {
313             return false;
314         }
315         View embeddedView = origin.view;
316         AccessibilityNodeProvider provider = embeddedView.getAccessibilityNodeProvider();
317         if (provider == null) {
318             return false;
319         }
320         return provider.performAction(origin.id, accessibilityAction, arguments);
321     }
322 
323     /**
324      * Returns a flutterID for an accessibility record, or null if no mapping exists.
325      *
326      * @param embeddedView the embedded view that the record is associated with.
327      */
328     @Nullable
getRecordFlutterId(@onNull View embeddedView, @NonNull AccessibilityRecord record)329     public Integer getRecordFlutterId(@NonNull View embeddedView, @NonNull AccessibilityRecord record) {
330         Long originPackedId = reflectionAccessors.getRecordSourceNodeId(record);
331         if (originPackedId == null) {
332             return null;
333         }
334         int originVirtualId = ReflectionAccessors.getVirtualNodeId(originPackedId);
335         return originToFlutterId.get(new ViewAndId(embeddedView, originVirtualId));
336     }
337 
338     /**
339      * Delegates a View#onHoverEvent event from the AccessibilityBridge to an embedded view.
340      *
341      * The pointer coordinates are translated to the embedded view's coordinate system.
342      */
onAccessibilityHoverEvent(int rootFlutterId, @NonNull MotionEvent event)343     public boolean onAccessibilityHoverEvent(int rootFlutterId, @NonNull MotionEvent event) {
344         ViewAndId origin = flutterIdToOrigin.get(rootFlutterId);
345         if (origin == null) {
346             return false;
347         }
348         Rect displayBounds = embeddedViewToDisplayBounds.get(origin.view);
349         int pointerCount = event.getPointerCount();
350         MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[pointerCount];
351         MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount];
352         for(int i = 0; i < event.getPointerCount(); i++) {
353             pointerProperties[i] = new MotionEvent.PointerProperties();
354             event.getPointerProperties(i, pointerProperties[i]);
355 
356             MotionEvent.PointerCoords originCoords = new MotionEvent.PointerCoords();
357             event.getPointerCoords(i, originCoords);
358 
359             pointerCoords[i] = new MotionEvent.PointerCoords((originCoords));
360             pointerCoords[i].x -= displayBounds.left;
361             pointerCoords[i].y -= displayBounds.top;
362 
363         }
364         MotionEvent translatedEvent = MotionEvent.obtain(
365                 event.getDownTime(),
366                 event.getEventTime(),
367                 event.getAction(),
368                 event.getPointerCount(),
369                 pointerProperties,
370                 pointerCoords,
371                 event.getMetaState(),
372                 event.getButtonState(),
373                 event.getXPrecision(),
374                 event.getYPrecision(),
375                 event.getDeviceId(),
376                 event.getEdgeFlags(),
377                 event.getSource(),
378                 event.getFlags()
379         );
380         return origin.view.dispatchGenericMotionEvent(translatedEvent);
381     }
382 
383     private static class ViewAndId {
384         final View view;
385         final int id;
386 
ViewAndId(View view, int id)387         private ViewAndId(View view, int id) {
388             this.view = view;
389             this.id = id;
390         }
391 
392         @Override
equals(Object o)393         public boolean equals(Object o) {
394             if (this == o) return true;
395             if (o == null || getClass() != o.getClass()) return false;
396             ViewAndId viewAndId = (ViewAndId) o;
397             return id == viewAndId.id &&
398                     view.equals(viewAndId.view);
399         }
400 
401         @Override
hashCode()402         public int hashCode() {
403             final int prime = 31;
404             int result = 1;
405             result = prime * result + view.hashCode();
406             result = prime * result + id;
407             return result;
408         }
409     }
410 
411     private static class ReflectionAccessors {
412         private @Nullable final Method getSourceNodeId;
413         private @Nullable final Method getParentNodeId;
414         private @Nullable final Method getRecordSourceNodeId;
415         private @Nullable final Method getChildId;
416         private @Nullable final Field childNodeIdsField;
417         private @Nullable final Method longArrayGetIndex;
418 
419         @SuppressLint("PrivateApi")
ReflectionAccessors()420         private ReflectionAccessors() {
421             Method getSourceNodeId = null;
422             Method getParentNodeId = null;
423             Method getRecordSourceNodeId = null;
424             Method getChildId = null;
425             Field childNodeIdsField = null;
426             Method longArrayGetIndex = null;
427             try {
428                 getSourceNodeId = AccessibilityNodeInfo.class.getMethod("getSourceNodeId");
429             } catch (NoSuchMethodException e) {
430                 Log.w(TAG, "can't invoke AccessibilityNodeInfo#getSourceNodeId with reflection");
431             }
432             try {
433                 getRecordSourceNodeId = AccessibilityRecord.class.getMethod("getSourceNodeId");
434             } catch (NoSuchMethodException e) {
435                 Log.w(TAG, "can't invoke AccessibiiltyRecord#getSourceNodeId with reflection");
436             }
437             // Reflection access is not allowed starting Android P on these methods.
438             if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) {
439                 try {
440                     getParentNodeId = AccessibilityNodeInfo.class.getMethod("getParentNodeId");
441                 } catch (NoSuchMethodException e) {
442                     Log.w(TAG, "can't invoke getParentNodeId with reflection");
443                 }
444                 // Starting P we extract the child id from the mChildNodeIds field (see getChildId
445                 // below).
446                 try {
447                     getChildId = AccessibilityNodeInfo.class.getMethod("getChildId", int.class);
448                 } catch (NoSuchMethodException e) {
449                     Log.w(TAG, "can't invoke getChildId with reflection");
450                 }
451             } else {
452                 try {
453                     childNodeIdsField = AccessibilityNodeInfo.class.getDeclaredField("mChildNodeIds");
454                     childNodeIdsField.setAccessible(true);
455                     // The private member is a private utility class to Android. We need to use
456                     // reflection to actually handle the data too.
457                     longArrayGetIndex = Class.forName("android.util.LongArray").getMethod("get", int.class);
458                 } catch (NoSuchFieldException | ClassNotFoundException | NoSuchMethodException | NullPointerException e) {
459                     Log.w(TAG, "can't access childNodeIdsField with reflection");
460                     childNodeIdsField = null;
461                 }
462             }
463             this.getSourceNodeId = getSourceNodeId;
464             this.getParentNodeId = getParentNodeId;
465             this.getRecordSourceNodeId = getRecordSourceNodeId;
466             this.getChildId = getChildId;
467             this.childNodeIdsField = childNodeIdsField;
468             this.longArrayGetIndex = longArrayGetIndex;
469         }
470 
471         /** Returns virtual node ID given packed node ID used internally in accessibility API. */
getVirtualNodeId(long nodeId)472         private static int getVirtualNodeId(long nodeId) {
473             return (int) (nodeId >> 32);
474         }
475 
476         @Nullable
getSourceNodeId(@onNull AccessibilityNodeInfo node)477         private Long getSourceNodeId(@NonNull AccessibilityNodeInfo node) {
478             if (getSourceNodeId == null) {
479                 return null;
480             }
481             try {
482                 return (Long) getSourceNodeId.invoke(node);
483             } catch (IllegalAccessException e) {
484                 Log.w(TAG, e);
485             } catch (InvocationTargetException e) {
486                 Log.w(TAG, e);
487             }
488             return null;
489         }
490 
491         @Nullable
getChildId(@onNull AccessibilityNodeInfo node, int child)492         private Long getChildId(@NonNull AccessibilityNodeInfo node, int child) {
493             if (getChildId == null && (childNodeIdsField == null || longArrayGetIndex == null)) {
494                 return null;
495             }
496             if (getChildId != null) {
497                 try {
498                     return (Long) getChildId.invoke(node, child);
499                 // Using identical separate catch blocks to comply with the following lint:
500                 // Error: Multi-catch with these reflection exceptions requires API level 19
501                 // (current min is 16) because they get compiled to the common but new super
502                 // type ReflectiveOperationException. As a workaround either create individual
503                 // catch statements, or catch Exception. [NewApi]
504                 } catch (IllegalAccessException e) {
505                     Log.w(TAG, e);
506                 } catch (InvocationTargetException e) {
507                     Log.w(TAG, e);
508                 }
509             } else {
510                 try {
511                     return (long) longArrayGetIndex.invoke(childNodeIdsField.get(node), child);
512                 // Using identical separate catch blocks to comply with the following lint:
513                 // Error: Multi-catch with these reflection exceptions requires API level 19
514                 // (current min is 16) because they get compiled to the common but new super
515                 // type ReflectiveOperationException. As a workaround either create individual
516                 // catch statements, or catch Exception. [NewApi]
517                 } catch (IllegalAccessException e) {
518                     Log.w(TAG, e);
519                 } catch (InvocationTargetException | ArrayIndexOutOfBoundsException e) {
520                     Log.w(TAG, e);
521                 }
522             }
523             return null;
524         }
525 
526         @Nullable
getParentNodeId(@onNull AccessibilityNodeInfo node)527         private Long getParentNodeId(@NonNull AccessibilityNodeInfo node) {
528             if (getParentNodeId != null) {
529                 try {
530                     return (long) getParentNodeId.invoke(node);
531                 // Using identical separate catch blocks to comply with the following lint:
532                 // Error: Multi-catch with these reflection exceptions requires API level 19
533                 // (current min is 16) because they get compiled to the common but new super
534                 // type ReflectiveOperationException. As a workaround either create individual
535                 // catch statements, or catch Exception. [NewApi]
536                 } catch (IllegalAccessException e) {
537                     Log.w(TAG, e);
538                 } catch (InvocationTargetException e) {
539                     Log.w(TAG, e);
540                 }
541             }
542 
543             // Fall back on reading the ID from a serialized data if we absolutely have to.
544             return yoinkParentIdFromParcel(node);
545         }
546 
547         // If this looks like it's failing, that's because it probably is. This method is relying on
548         // the implementation details of `AccessibilityNodeInfo#writeToParcel` in order to find the
549         // particular bit in the opaque parcel that represents mParentNodeId. If the implementation
550         // details change from our assumptions in this method, this will silently break.
551         @Nullable
yoinkParentIdFromParcel(AccessibilityNodeInfo node)552         private static Long yoinkParentIdFromParcel(AccessibilityNodeInfo node) {
553             if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
554                 Log.w(TAG, "Unexpected Android version. Unable to find the parent ID.");
555                 return null;
556             }
557 
558             // We're creating a copy here because writing a node to a parcel recycles it. Objects
559             // are passed by reference in Java. So even though this method doesn't seem to use the
560             // node again, it's really used in other methods that would throw exceptions if we
561             // recycle it here.
562             AccessibilityNodeInfo copy = AccessibilityNodeInfo.obtain(node);
563             final Parcel parcel = Parcel.obtain();
564             parcel.setDataPosition(0);
565             copy.writeToParcel(parcel, /*flags=*/ 0);
566             Long parentNodeId = null;
567             // Match the internal logic that sets where mParentId actually ends up finally living.
568             // This logic should match
569             // https://android.googlesource.com/platform/frameworks/base/+/0b5ca24a4/core/java/android/view/accessibility/AccessibilityNodeInfo.java#3524.
570             parcel.setDataPosition(0);
571             long nonDefaultFields = parcel.readLong();
572             int fieldIndex = 0;
573             if (isBitSet(nonDefaultFields, fieldIndex++)) {
574                 parcel.readInt(); // mIsSealed
575             }
576             if (isBitSet(nonDefaultFields, fieldIndex++)) {
577                 parcel.readLong(); // mSourceNodeId
578             }
579             if (isBitSet(nonDefaultFields, fieldIndex++)) {
580                 parcel.readInt();  // mWindowId
581             }
582             if (isBitSet(nonDefaultFields, fieldIndex++)) {
583                 parentNodeId = parcel.readLong();
584             }
585 
586             parcel.recycle();
587             return parentNodeId;
588         }
589 
isBitSet(long flags, int bitIndex)590         private static boolean isBitSet(long flags, int bitIndex) {
591             return (flags & (1L << bitIndex)) != 0;
592         }
593 
594         @Nullable
getRecordSourceNodeId(@onNull AccessibilityRecord node)595         private Long getRecordSourceNodeId(@NonNull AccessibilityRecord node) {
596             if (getRecordSourceNodeId == null) {
597                 return null;
598             }
599             try {
600                 return (Long) getRecordSourceNodeId.invoke(node);
601             } catch (IllegalAccessException e) {
602                 Log.w(TAG, e);
603             } catch (InvocationTargetException e) {
604                 Log.w(TAG, e);
605             }
606             return null;
607         }
608     }
609 }
610