| // Copyright 2013 The Flutter Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| package io.flutter.view; | |
| import android.annotation.SuppressLint; | |
| import android.graphics.Rect; | |
| import android.os.Build; | |
| import android.os.Bundle; | |
| import android.os.Parcel; | |
| import android.util.SparseArray; | |
| import android.view.MotionEvent; | |
| import android.view.View; | |
| import android.view.accessibility.AccessibilityEvent; | |
| import android.view.accessibility.AccessibilityNodeInfo; | |
| import android.view.accessibility.AccessibilityNodeProvider; | |
| import android.view.accessibility.AccessibilityRecord; | |
| import androidx.annotation.Keep; | |
| import androidx.annotation.NonNull; | |
| import androidx.annotation.Nullable; | |
| import io.flutter.Log; | |
| import java.lang.reflect.Field; | |
| import java.lang.reflect.InvocationTargetException; | |
| import java.lang.reflect.Method; | |
| import java.util.HashMap; | |
| import java.util.Map; | |
| /** | |
| * Facilitates embedding of platform views in the accessibility tree generated by the accessibility | |
| * bridge. | |
| * | |
| * <p>Embedding is done by mirroring the accessibility tree of the platform view as a subtree of the | |
| * flutter accessibility tree. | |
| * | |
| * <p>This class relies on hidden system APIs to extract the accessibility information and does not | |
| * work starting Android P; If the reflection accessors are not available we fail silently by | |
| * embedding a null node, the app continues working but the accessibility information for the | |
| * platform view will not be embedded. | |
| * | |
| * <p>We use the term `flutterId` for virtual accessibility node IDs in the FlutterView tree, and | |
| * the term `originId` for the virtual accessibility node IDs in the platform view's tree. | |
| * Internally this class maintains a bidirectional mapping between `flutterId`s and the | |
| * corresponding platform view and `originId`. | |
| */ | |
| @Keep | |
| class AccessibilityViewEmbedder { | |
| private static final String TAG = "AccessibilityBridge"; | |
| private final ReflectionAccessors reflectionAccessors; | |
| // The view to which the platform view is embedded, this is typically FlutterView. | |
| private final View rootAccessibilityView; | |
| // Maps a flutterId to the corresponding platform view and originId. | |
| private final SparseArray<ViewAndId> flutterIdToOrigin; | |
| // Maps a platform view and originId to a corresponding flutterID. | |
| private final Map<ViewAndId, Integer> originToFlutterId; | |
| // Maps an embedded view to it's screen bounds. | |
| // This is used to translate the coordinates of the accessibility node subtree to the main | |
| // display's coordinate | |
| // system. | |
| private final Map<View, Rect> embeddedViewToDisplayBounds; | |
| private int nextFlutterId; | |
| AccessibilityViewEmbedder(@NonNull View rootAccessibiiltyView, int firstVirtualNodeId) { | |
| reflectionAccessors = new ReflectionAccessors(); | |
| flutterIdToOrigin = new SparseArray<>(); | |
| this.rootAccessibilityView = rootAccessibiiltyView; | |
| nextFlutterId = firstVirtualNodeId; | |
| originToFlutterId = new HashMap<>(); | |
| embeddedViewToDisplayBounds = new HashMap<>(); | |
| } | |
| /** | |
| * Returns the root accessibility node for an embedded platform view. | |
| * | |
| * @param flutterId the virtual accessibility ID for the node in flutter accessibility tree | |
| * @param displayBounds the display bounds for the node in screen coordinates | |
| */ | |
| public AccessibilityNodeInfo getRootNode( | |
| @NonNull View embeddedView, int flutterId, @NonNull Rect displayBounds) { | |
| AccessibilityNodeInfo originNode = embeddedView.createAccessibilityNodeInfo(); | |
| Long originPackedId = reflectionAccessors.getSourceNodeId(originNode); | |
| if (originPackedId == null) { | |
| return null; | |
| } | |
| embeddedViewToDisplayBounds.put(embeddedView, displayBounds); | |
| int originId = ReflectionAccessors.getVirtualNodeId(originPackedId); | |
| cacheVirtualIdMappings(embeddedView, originId, flutterId); | |
| return convertToFlutterNode(originNode, flutterId, embeddedView); | |
| } | |
| /** Creates the accessibility node info for the node identified with `flutterId`. */ | |
| @Nullable | |
| public AccessibilityNodeInfo createAccessibilityNodeInfo(int flutterId) { | |
| ViewAndId origin = flutterIdToOrigin.get(flutterId); | |
| if (origin == null) { | |
| return null; | |
| } | |
| if (!embeddedViewToDisplayBounds.containsKey(origin.view)) { | |
| // This might happen if the embedded view is sending accessibility event before the first | |
| // Flutter semantics | |
| // tree was sent to the accessibility bridge. In this case we don't return a node as we do not | |
| // know the | |
| // bounds yet. | |
| // https://github.com/flutter/flutter/issues/30068 | |
| return null; | |
| } | |
| AccessibilityNodeProvider provider = origin.view.getAccessibilityNodeProvider(); | |
| if (provider == null) { | |
| // The provider is null for views that don't have a virtual accessibility tree. | |
| // We currently only support embedding virtual hierarchies in the Flutter tree. | |
| // TODO(amirh): support embedding non virtual hierarchies. | |
| // https://github.com/flutter/flutter/issues/29717 | |
| return null; | |
| } | |
| AccessibilityNodeInfo originNode = | |
| origin.view.getAccessibilityNodeProvider().createAccessibilityNodeInfo(origin.id); | |
| if (originNode == null) { | |
| return null; | |
| } | |
| return convertToFlutterNode(originNode, flutterId, origin.view); | |
| } | |
| /* | |
| * Creates an AccessibilityNodeInfo that can be attached to the Flutter accessibility tree and is equivalent to | |
| * originNode(which belongs to embeddedView). The virtual ID for the created node will be flutterId. | |
| */ | |
| @NonNull | |
| private AccessibilityNodeInfo convertToFlutterNode( | |
| @NonNull AccessibilityNodeInfo originNode, int flutterId, @NonNull View embeddedView) { | |
| AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(rootAccessibilityView, flutterId); | |
| result.setPackageName(rootAccessibilityView.getContext().getPackageName()); | |
| result.setSource(rootAccessibilityView, flutterId); | |
| result.setClassName(originNode.getClassName()); | |
| Rect displayBounds = embeddedViewToDisplayBounds.get(embeddedView); | |
| copyAccessibilityFields(originNode, result); | |
| setFlutterNodesTranslateBounds(originNode, displayBounds, result); | |
| addChildrenToFlutterNode(originNode, embeddedView, result); | |
| setFlutterNodeParent(originNode, embeddedView, result); | |
| return result; | |
| } | |
| private void setFlutterNodeParent( | |
| @NonNull AccessibilityNodeInfo originNode, | |
| @NonNull View embeddedView, | |
| @NonNull AccessibilityNodeInfo result) { | |
| Long parentOriginPackedId = reflectionAccessors.getParentNodeId(originNode); | |
| if (parentOriginPackedId == null) { | |
| return; | |
| } | |
| int parentOriginId = ReflectionAccessors.getVirtualNodeId(parentOriginPackedId); | |
| Integer parentFlutterId = originToFlutterId.get(new ViewAndId(embeddedView, parentOriginId)); | |
| if (parentFlutterId != null) { | |
| result.setParent(rootAccessibilityView, parentFlutterId); | |
| } | |
| } | |
| private void addChildrenToFlutterNode( | |
| @NonNull AccessibilityNodeInfo originNode, | |
| @NonNull View embeddedView, | |
| @NonNull AccessibilityNodeInfo resultNode) { | |
| for (int i = 0; i < originNode.getChildCount(); i++) { | |
| Long originPackedId = reflectionAccessors.getChildId(originNode, i); | |
| if (originPackedId == null) { | |
| continue; | |
| } | |
| int originId = ReflectionAccessors.getVirtualNodeId(originPackedId); | |
| ViewAndId origin = new ViewAndId(embeddedView, originId); | |
| int childFlutterId; | |
| if (originToFlutterId.containsKey(origin)) { | |
| childFlutterId = originToFlutterId.get(origin); | |
| } else { | |
| childFlutterId = nextFlutterId++; | |
| cacheVirtualIdMappings(embeddedView, originId, childFlutterId); | |
| } | |
| resultNode.addChild(rootAccessibilityView, childFlutterId); | |
| } | |
| } | |
| // Caches a bidirectional mapping of (embeddedView, originId)<-->flutterId. | |
| // Where originId is a virtual node ID in the embeddedView's tree, and flutterId is the ID | |
| // of the corresponding node in the Flutter virtual accessibility nodes tree. | |
| private void cacheVirtualIdMappings(@NonNull View embeddedView, int originId, int flutterId) { | |
| ViewAndId origin = new ViewAndId(embeddedView, originId); | |
| originToFlutterId.put(origin, flutterId); | |
| flutterIdToOrigin.put(flutterId, origin); | |
| } | |
| // Supressing deprecation warning for AccessibilityNodeInfo#getBoundsinParent and | |
| // AccessibilityNodeInfo#getBoundsinParent as we are copying the platform view's | |
| // accessibility node and we should not lose any available bounds information. | |
| @SuppressWarnings("deprecation") | |
| private void setFlutterNodesTranslateBounds( | |
| @NonNull AccessibilityNodeInfo originNode, | |
| @NonNull Rect displayBounds, | |
| @NonNull AccessibilityNodeInfo resultNode) { | |
| Rect boundsInParent = new Rect(); | |
| originNode.getBoundsInParent(boundsInParent); | |
| resultNode.setBoundsInParent(boundsInParent); | |
| Rect boundsInScreen = new Rect(); | |
| originNode.getBoundsInScreen(boundsInScreen); | |
| boundsInScreen.offset(displayBounds.left, displayBounds.top); | |
| resultNode.setBoundsInScreen(boundsInScreen); | |
| } | |
| private void copyAccessibilityFields( | |
| @NonNull AccessibilityNodeInfo input, @NonNull AccessibilityNodeInfo output) { | |
| output.setAccessibilityFocused(input.isAccessibilityFocused()); | |
| output.setCheckable(input.isCheckable()); | |
| output.setChecked(input.isChecked()); | |
| output.setContentDescription(input.getContentDescription()); | |
| output.setEnabled(input.isEnabled()); | |
| output.setClickable(input.isClickable()); | |
| output.setFocusable(input.isFocusable()); | |
| output.setFocused(input.isFocused()); | |
| output.setLongClickable(input.isLongClickable()); | |
| output.setMovementGranularities(input.getMovementGranularities()); | |
| output.setPassword(input.isPassword()); | |
| output.setScrollable(input.isScrollable()); | |
| output.setSelected(input.isSelected()); | |
| output.setText(input.getText()); | |
| output.setVisibleToUser(input.isVisibleToUser()); | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { | |
| output.setEditable(input.isEditable()); | |
| } | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { | |
| output.setCanOpenPopup(input.canOpenPopup()); | |
| output.setCollectionInfo(input.getCollectionInfo()); | |
| output.setCollectionItemInfo(input.getCollectionItemInfo()); | |
| output.setContentInvalid(input.isContentInvalid()); | |
| output.setDismissable(input.isDismissable()); | |
| output.setInputType(input.getInputType()); | |
| output.setLiveRegion(input.getLiveRegion()); | |
| output.setMultiLine(input.isMultiLine()); | |
| output.setRangeInfo(input.getRangeInfo()); | |
| } | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | |
| output.setError(input.getError()); | |
| output.setMaxTextLength(input.getMaxTextLength()); | |
| } | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | |
| output.setContextClickable(input.isContextClickable()); | |
| // TODO(amirh): copy traversal before and after. | |
| // https://github.com/flutter/flutter/issues/29718 | |
| } | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | |
| output.setDrawingOrder(input.getDrawingOrder()); | |
| output.setImportantForAccessibility(input.isImportantForAccessibility()); | |
| } | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | |
| output.setAvailableExtraData(input.getAvailableExtraData()); | |
| output.setHintText(input.getHintText()); | |
| output.setShowingHintText(input.isShowingHintText()); | |
| } | |
| } | |
| /** | |
| * Delegates an AccessibilityNodeProvider#requestSendAccessibilityEvent from the | |
| * AccessibilityBridge to the embedded view. | |
| * | |
| * @return True if the event was sent. | |
| */ | |
| public boolean requestSendAccessibilityEvent( | |
| @NonNull View embeddedView, @NonNull View eventOrigin, @NonNull AccessibilityEvent event) { | |
| AccessibilityEvent translatedEvent = AccessibilityEvent.obtain(event); | |
| Long originPackedId = reflectionAccessors.getRecordSourceNodeId(event); | |
| if (originPackedId == null) { | |
| return false; | |
| } | |
| int originVirtualId = ReflectionAccessors.getVirtualNodeId(originPackedId); | |
| Integer flutterId = originToFlutterId.get(new ViewAndId(embeddedView, originVirtualId)); | |
| if (flutterId == null) { | |
| flutterId = nextFlutterId++; | |
| cacheVirtualIdMappings(embeddedView, originVirtualId, flutterId); | |
| } | |
| translatedEvent.setSource(rootAccessibilityView, flutterId); | |
| translatedEvent.setClassName(event.getClassName()); | |
| translatedEvent.setPackageName(event.getPackageName()); | |
| for (int i = 0; i < translatedEvent.getRecordCount(); i++) { | |
| AccessibilityRecord record = translatedEvent.getRecord(i); | |
| Long recordOriginPackedId = reflectionAccessors.getRecordSourceNodeId(record); | |
| if (recordOriginPackedId == null) { | |
| return false; | |
| } | |
| int recordOriginVirtualID = ReflectionAccessors.getVirtualNodeId(recordOriginPackedId); | |
| ViewAndId originViewAndId = new ViewAndId(embeddedView, recordOriginVirtualID); | |
| if (!originToFlutterId.containsKey(originViewAndId)) { | |
| return false; | |
| } | |
| int recordFlutterId = originToFlutterId.get(originViewAndId); | |
| record.setSource(rootAccessibilityView, recordFlutterId); | |
| } | |
| return rootAccessibilityView | |
| .getParent() | |
| .requestSendAccessibilityEvent(eventOrigin, translatedEvent); | |
| } | |
| /** | |
| * Delegates an @{link AccessibilityNodeProvider#performAction} from the AccessibilityBridge to | |
| * the embedded view's accessibility node provider. | |
| * | |
| * @return True if the action was performed. | |
| */ | |
| public boolean performAction(int flutterId, int accessibilityAction, @Nullable Bundle arguments) { | |
| ViewAndId origin = flutterIdToOrigin.get(flutterId); | |
| if (origin == null) { | |
| return false; | |
| } | |
| View embeddedView = origin.view; | |
| AccessibilityNodeProvider provider = embeddedView.getAccessibilityNodeProvider(); | |
| if (provider == null) { | |
| return false; | |
| } | |
| return provider.performAction(origin.id, accessibilityAction, arguments); | |
| } | |
| /** | |
| * Returns a flutterID for an accessibility record, or null if no mapping exists. | |
| * | |
| * @param embeddedView the embedded view that the record is associated with. | |
| */ | |
| @Nullable | |
| public Integer getRecordFlutterId( | |
| @NonNull View embeddedView, @NonNull AccessibilityRecord record) { | |
| Long originPackedId = reflectionAccessors.getRecordSourceNodeId(record); | |
| if (originPackedId == null) { | |
| return null; | |
| } | |
| int originVirtualId = ReflectionAccessors.getVirtualNodeId(originPackedId); | |
| return originToFlutterId.get(new ViewAndId(embeddedView, originVirtualId)); | |
| } | |
| /** | |
| * Delegates a View#onHoverEvent event from the AccessibilityBridge to an embedded view. | |
| * | |
| * <p>The pointer coordinates are translated to the embedded view's coordinate system. | |
| */ | |
| public boolean onAccessibilityHoverEvent(int rootFlutterId, @NonNull MotionEvent event) { | |
| ViewAndId origin = flutterIdToOrigin.get(rootFlutterId); | |
| if (origin == null) { | |
| return false; | |
| } | |
| Rect displayBounds = embeddedViewToDisplayBounds.get(origin.view); | |
| int pointerCount = event.getPointerCount(); | |
| MotionEvent.PointerProperties[] pointerProperties = | |
| new MotionEvent.PointerProperties[pointerCount]; | |
| MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[pointerCount]; | |
| for (int i = 0; i < event.getPointerCount(); i++) { | |
| pointerProperties[i] = new MotionEvent.PointerProperties(); | |
| event.getPointerProperties(i, pointerProperties[i]); | |
| MotionEvent.PointerCoords originCoords = new MotionEvent.PointerCoords(); | |
| event.getPointerCoords(i, originCoords); | |
| pointerCoords[i] = new MotionEvent.PointerCoords(originCoords); | |
| pointerCoords[i].x -= displayBounds.left; | |
| pointerCoords[i].y -= displayBounds.top; | |
| } | |
| MotionEvent translatedEvent = | |
| MotionEvent.obtain( | |
| event.getDownTime(), | |
| event.getEventTime(), | |
| event.getAction(), | |
| event.getPointerCount(), | |
| pointerProperties, | |
| pointerCoords, | |
| event.getMetaState(), | |
| event.getButtonState(), | |
| event.getXPrecision(), | |
| event.getYPrecision(), | |
| event.getDeviceId(), | |
| event.getEdgeFlags(), | |
| event.getSource(), | |
| event.getFlags()); | |
| return origin.view.dispatchGenericMotionEvent(translatedEvent); | |
| } | |
| /** | |
| * Returns the View that contains the accessibility node identified by the provided flutterId or | |
| * null if it doesn't belong to a view. | |
| */ | |
| public View platformViewOfNode(int flutterId) { | |
| ViewAndId viewAndId = flutterIdToOrigin.get(flutterId); | |
| if (viewAndId == null) { | |
| return null; | |
| } | |
| return viewAndId.view; | |
| } | |
| private static class ViewAndId { | |
| final View view; | |
| final int id; | |
| private ViewAndId(View view, int id) { | |
| this.view = view; | |
| this.id = id; | |
| } | |
| @Override | |
| public boolean equals(Object o) { | |
| if (this == o) return true; | |
| if (!(o instanceof ViewAndId)) return false; | |
| ViewAndId viewAndId = (ViewAndId) o; | |
| return id == viewAndId.id && view.equals(viewAndId.view); | |
| } | |
| @Override | |
| public int hashCode() { | |
| final int prime = 31; | |
| int result = 1; | |
| result = prime * result + view.hashCode(); | |
| result = prime * result + id; | |
| return result; | |
| } | |
| } | |
| private static class ReflectionAccessors { | |
| private @Nullable final Method getSourceNodeId; | |
| private @Nullable final Method getParentNodeId; | |
| private @Nullable final Method getRecordSourceNodeId; | |
| private @Nullable final Method getChildId; | |
| private @Nullable final Field childNodeIdsField; | |
| private @Nullable final Method longArrayGetIndex; | |
| @SuppressLint("PrivateApi") | |
| private ReflectionAccessors() { | |
| Method getSourceNodeId = null; | |
| Method getParentNodeId = null; | |
| Method getRecordSourceNodeId = null; | |
| Method getChildId = null; | |
| Field childNodeIdsField = null; | |
| Method longArrayGetIndex = null; | |
| try { | |
| getSourceNodeId = AccessibilityNodeInfo.class.getMethod("getSourceNodeId"); | |
| } catch (NoSuchMethodException e) { | |
| Log.w(TAG, "can't invoke AccessibilityNodeInfo#getSourceNodeId with reflection"); | |
| } | |
| try { | |
| getRecordSourceNodeId = AccessibilityRecord.class.getMethod("getSourceNodeId"); | |
| } catch (NoSuchMethodException e) { | |
| Log.w(TAG, "can't invoke AccessibiiltyRecord#getSourceNodeId with reflection"); | |
| } | |
| // Reflection access is not allowed starting Android P on these methods. | |
| if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) { | |
| try { | |
| getParentNodeId = AccessibilityNodeInfo.class.getMethod("getParentNodeId"); | |
| } catch (NoSuchMethodException e) { | |
| Log.w(TAG, "can't invoke getParentNodeId with reflection"); | |
| } | |
| // Starting P we extract the child id from the mChildNodeIds field (see getChildId | |
| // below). | |
| try { | |
| getChildId = AccessibilityNodeInfo.class.getMethod("getChildId", int.class); | |
| } catch (NoSuchMethodException e) { | |
| Log.w(TAG, "can't invoke getChildId with reflection"); | |
| } | |
| } else { | |
| try { | |
| childNodeIdsField = AccessibilityNodeInfo.class.getDeclaredField("mChildNodeIds"); | |
| childNodeIdsField.setAccessible(true); | |
| // The private member is a private utility class to Android. We need to use | |
| // reflection to actually handle the data too. | |
| longArrayGetIndex = Class.forName("android.util.LongArray").getMethod("get", int.class); | |
| } catch (NoSuchFieldException | |
| | ClassNotFoundException | |
| | NoSuchMethodException | |
| | NullPointerException e) { | |
| Log.w(TAG, "can't access childNodeIdsField with reflection"); | |
| childNodeIdsField = null; | |
| } | |
| } | |
| this.getSourceNodeId = getSourceNodeId; | |
| this.getParentNodeId = getParentNodeId; | |
| this.getRecordSourceNodeId = getRecordSourceNodeId; | |
| this.getChildId = getChildId; | |
| this.childNodeIdsField = childNodeIdsField; | |
| this.longArrayGetIndex = longArrayGetIndex; | |
| } | |
| /** Returns virtual node ID given packed node ID used internally in accessibility API. */ | |
| private static int getVirtualNodeId(long nodeId) { | |
| return (int) (nodeId >> 32); | |
| } | |
| @Nullable | |
| private Long getSourceNodeId(@NonNull AccessibilityNodeInfo node) { | |
| if (getSourceNodeId == null) { | |
| return null; | |
| } | |
| try { | |
| return (Long) getSourceNodeId.invoke(node); | |
| } catch (IllegalAccessException e) { | |
| Log.w(TAG, "Failed to access getSourceNodeId method.", e); | |
| } catch (InvocationTargetException e) { | |
| Log.w(TAG, "The getSourceNodeId method threw an exception when invoked.", e); | |
| } | |
| return null; | |
| } | |
| @Nullable | |
| private Long getChildId(@NonNull AccessibilityNodeInfo node, int child) { | |
| if (getChildId == null && (childNodeIdsField == null || longArrayGetIndex == null)) { | |
| return null; | |
| } | |
| if (getChildId != null) { | |
| try { | |
| return (Long) getChildId.invoke(node, child); | |
| // Using identical separate catch blocks to comply with the following lint: | |
| // Error: Multi-catch with these reflection exceptions requires API level 19 | |
| // (current min is 16) because they get compiled to the common but new super | |
| // type ReflectiveOperationException. As a workaround either create individual | |
| // catch statements, or catch Exception. [NewApi] | |
| } catch (IllegalAccessException e) { | |
| Log.w(TAG, "Failed to access getChildId method.", e); | |
| } catch (InvocationTargetException e) { | |
| Log.w(TAG, "The getChildId method threw an exception when invoked.", e); | |
| } | |
| } else { | |
| try { | |
| return (long) longArrayGetIndex.invoke(childNodeIdsField.get(node), child); | |
| // Using identical separate catch blocks to comply with the following lint: | |
| // Error: Multi-catch with these reflection exceptions requires API level 19 | |
| // (current min is 16) because they get compiled to the common but new super | |
| // type ReflectiveOperationException. As a workaround either create individual | |
| // catch statements, or catch Exception. [NewApi] | |
| } catch (IllegalAccessException e) { | |
| Log.w(TAG, "Failed to access longArrayGetIndex method or the childNodeId field.", e); | |
| } catch (InvocationTargetException | ArrayIndexOutOfBoundsException e) { | |
| Log.w(TAG, "The longArrayGetIndex method threw an exception when invoked.", e); | |
| } | |
| } | |
| return null; | |
| } | |
| @Nullable | |
| private Long getParentNodeId(@NonNull AccessibilityNodeInfo node) { | |
| if (getParentNodeId != null) { | |
| try { | |
| return (long) getParentNodeId.invoke(node); | |
| // Using identical separate catch blocks to comply with the following lint: | |
| // Error: Multi-catch with these reflection exceptions requires API level 19 | |
| // (current min is 16) because they get compiled to the common but new super | |
| // type ReflectiveOperationException. As a workaround either create individual | |
| // catch statements, or catch Exception. [NewApi] | |
| } catch (IllegalAccessException e) { | |
| Log.w(TAG, "Failed to access getParentNodeId method.", e); | |
| } catch (InvocationTargetException e) { | |
| Log.w(TAG, "The getParentNodeId method threw an exception when invoked.", e); | |
| } | |
| } | |
| // Fall back on reading the ID from a serialized data if we absolutely have to. | |
| return yoinkParentIdFromParcel(node); | |
| } | |
| // If this looks like it's failing, that's because it probably is. This method is relying on | |
| // the implementation details of `AccessibilityNodeInfo#writeToParcel` in order to find the | |
| // particular bit in the opaque parcel that represents mParentNodeId. If the implementation | |
| // details change from our assumptions in this method, this will silently break. | |
| @Nullable | |
| private static Long yoinkParentIdFromParcel(AccessibilityNodeInfo node) { | |
| if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { | |
| Log.w(TAG, "Unexpected Android version. Unable to find the parent ID."); | |
| return null; | |
| } | |
| // We're creating a copy here because writing a node to a parcel recycles it. Objects | |
| // are passed by reference in Java. So even though this method doesn't seem to use the | |
| // node again, it's really used in other methods that would throw exceptions if we | |
| // recycle it here. | |
| AccessibilityNodeInfo copy = AccessibilityNodeInfo.obtain(node); | |
| final Parcel parcel = Parcel.obtain(); | |
| parcel.setDataPosition(0); | |
| copy.writeToParcel(parcel, /*flags=*/ 0); | |
| Long parentNodeId = null; | |
| // Match the internal logic that sets where mParentId actually ends up finally living. | |
| // This logic should match | |
| // https://android.googlesource.com/platform/frameworks/base/+/0b5ca24a4/core/java/android/view/accessibility/AccessibilityNodeInfo.java#3524. | |
| parcel.setDataPosition(0); | |
| long nonDefaultFields = parcel.readLong(); | |
| int fieldIndex = 0; | |
| if (isBitSet(nonDefaultFields, fieldIndex++)) { | |
| parcel.readInt(); // mIsSealed | |
| } | |
| if (isBitSet(nonDefaultFields, fieldIndex++)) { | |
| parcel.readLong(); // mSourceNodeId | |
| } | |
| if (isBitSet(nonDefaultFields, fieldIndex++)) { | |
| parcel.readInt(); // mWindowId | |
| } | |
| if (isBitSet(nonDefaultFields, fieldIndex++)) { | |
| parentNodeId = parcel.readLong(); | |
| } | |
| parcel.recycle(); | |
| return parentNodeId; | |
| } | |
| private static boolean isBitSet(long flags, int bitIndex) { | |
| return (flags & (1L << bitIndex)) != 0; | |
| } | |
| @Nullable | |
| private Long getRecordSourceNodeId(@NonNull AccessibilityRecord node) { | |
| if (getRecordSourceNodeId == null) { | |
| return null; | |
| } | |
| try { | |
| return (Long) getRecordSourceNodeId.invoke(node); | |
| } catch (IllegalAccessException e) { | |
| Log.w(TAG, "Failed to access the getRecordSourceNodeId method.", e); | |
| } catch (InvocationTargetException e) { | |
| Log.w(TAG, "The getRecordSourceNodeId method threw an exception when invoked.", e); | |
| } | |
| return null; | |
| } | |
| } | |
| } |