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