Delegate a11y events and action to/from embedded Android platform views. (flutter/engine#8250)

Delegate a11y events and action to/from embedded Android platfrom views.

This handles delegation of:
  * AccessibilityNodeProvider#performAction
  * ViewGroup#requestSendAccessibilityEvent
  * View#onHoverEvent

Additionally updates the currently input accessibility focused node state that is
tracked by the a11y bridge when an embedded view's node is focused.
This commit is contained in:
Amir Hardon 2019-03-25 14:26:49 -07:00 committed by GitHub
parent 0d3dd80099
commit 968cefaef1
8 changed files with 376 additions and 38 deletions

View File

@ -511,6 +511,7 @@ FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/StandardM
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/common/StringCodec.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/InputConnectionAdaptor.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/AccessibilityEventsDelegate.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformView.java
FILE: ../../../flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewFactory.java

View File

@ -153,6 +153,7 @@ java_library("flutter_shell_java") {
"io/flutter/plugin/common/StringCodec.java",
"io/flutter/plugin/editing/InputConnectionAdaptor.java",
"io/flutter/plugin/editing/TextInputPlugin.java",
"io/flutter/plugin/platform/AccessibilityEventsDelegate.java",
"io/flutter/plugin/platform/PlatformPlugin.java",
"io/flutter/plugin/platform/PlatformView.java",
"io/flutter/plugin/platform/PlatformViewFactory.java",

View File

@ -0,0 +1,49 @@
// 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.plugin.platform;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import io.flutter.view.AccessibilityBridge;
/**
* Delegates accessibility events to the currently attached accessibility bridge if one is attached.
*/
class AccessibilityEventsDelegate {
private AccessibilityBridge accessibilityBridge;
/**
* Delegates handling of {@link android.view.ViewParent#requestSendAccessibilityEvent} to the accessibility bridge.
*
* This is a no-op if there is no accessibility delegate set.
*
* This is used by embedded platform views to propagate accessibility events from their view hierarchy to the
* accessibility bridge.
*
* As the embedded view doesn't have to be the only View in the embedded hierarchy (it can have child views) and the
* event might have been originated from any view in this hierarchy, this method gets both a reference to the
* embedded platform view, and a reference to the view from its hierarchy that sent the event.
*
* @param embeddedView the embedded platform view for which the event is delegated
* @param eventOrigin the view in the embedded view's hierarchy that sent the event.
* @return True if the event was sent.
*/
public boolean requestSendAccessibilityEvent(@NonNull View embeddedView, @NonNull View eventOrigin, @NonNull AccessibilityEvent event) {
if (accessibilityBridge == null) {
return false;
}
return accessibilityBridge.externalViewRequestSendAccessibilityEvent(embeddedView, eventOrigin, event);
}
/*
* This setter should only be used directly in PlatformViewsController when attached/detached to an accessibility
* bridge.
*/
void setAccessibilityBridge(@Nullable AccessibilityBridge accessibilityBridge) {
this.accessibilityBridge = accessibilityBridge;
}
}

View File

@ -52,13 +52,14 @@ public class PlatformViewsController implements MethodChannel.MethodCallHandler,
private BinaryMessenger mMessenger;
// The accessibility bridge to which accessibility events form the platform views will be dispatched.
private AccessibilityBridge accessibilityBridge;
private final AccessibilityEventsDelegate mAccessibilityEventsDelegate;
private final HashMap<Integer, VirtualDisplayController> vdControllers;
public PlatformViewsController() {
mRegistry = new PlatformViewRegistryImpl();
vdControllers = new HashMap<>();
mAccessibilityEventsDelegate = new AccessibilityEventsDelegate();
}
/**
@ -100,12 +101,12 @@ public class PlatformViewsController implements MethodChannel.MethodCallHandler,
@Override
public void attachAccessibilityBridge(AccessibilityBridge accessibilityBridge) {
this.accessibilityBridge = accessibilityBridge;
mAccessibilityEventsDelegate.setAccessibilityBridge(accessibilityBridge);
}
@Override
public void detachAccessibiltyBridge() {
this.accessibilityBridge = null;
mAccessibilityEventsDelegate.setAccessibilityBridge(null);
}
public PlatformViewRegistry getRegistry() {
@ -201,6 +202,7 @@ public class PlatformViewsController implements MethodChannel.MethodCallHandler,
TextureRegistry.SurfaceTextureEntry textureEntry = mTextureRegistry.createSurfaceTexture();
VirtualDisplayController vdController = VirtualDisplayController.create(
mContext,
mAccessibilityEventsDelegate,
viewFactory,
textureEntry,
toPhysicalPixels(logicalWidth),

View File

@ -13,6 +13,7 @@ import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.*;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import java.lang.reflect.*;
@ -57,6 +58,9 @@ class SingleViewPresentation extends Presentation {
private final PlatformViewFactory mViewFactory;
// A reference to the current accessibility bridge to which accessibility events will be delegated.
private final AccessibilityEventsDelegate mAccessibilityEventsDelegate;
// This is the view id assigned by the Flutter framework to the embedded view, we keep it here
// so when we create the platform view we can tell it its view id.
private int mViewId;
@ -67,7 +71,7 @@ class SingleViewPresentation extends Presentation {
// The root view for the presentation, it has 2 childs: mContainer which contains the embedded view, and
// mFakeWindowRootView which contains views that were added directly to the presentation's window manager.
private FrameLayout mRootView;
private AccessibilityDelegatingFrameLayout mRootView;
// Contains the embedded platform view (mView.getView()) when it is attached to the presentation.
private FrameLayout mContainer;
@ -82,10 +86,13 @@ class SingleViewPresentation extends Presentation {
Context outerContext,
Display display,
PlatformViewFactory viewFactory,
AccessibilityEventsDelegate accessibilityEventsDelegate,
int viewId,
Object createParams) {
Object createParams
) {
super(outerContext, display);
mViewFactory = viewFactory;
mAccessibilityEventsDelegate = accessibilityEventsDelegate;
mViewId = viewId;
mCreateParams = createParams;
mState = new PresentationState();
@ -102,8 +109,14 @@ class SingleViewPresentation extends Presentation {
* <p>The display's density must match the density of the context used
* when the view was created.
*/
public SingleViewPresentation(Context outerContext, Display display, PresentationState state) {
public SingleViewPresentation(
Context outerContext,
Display display,
AccessibilityEventsDelegate accessibilityEventsDelegate,
PresentationState state
) {
super(outerContext, display);
mAccessibilityEventsDelegate = accessibilityEventsDelegate;
mViewFactory = null;
mState = state;
getWindow().setFlags(
@ -130,8 +143,9 @@ class SingleViewPresentation extends Presentation {
mState.mView = mViewFactory.create(context, mViewId, mCreateParams);
}
mContainer.addView(mState.mView.getView());
mRootView = new FrameLayout(getContext());
View embeddedView = mState.mView.getView();
mContainer.addView(embeddedView);
mRootView = new AccessibilityDelegatingFrameLayout(getContext(), mAccessibilityEventsDelegate, embeddedView);
mRootView.addView(mContainer);
mRootView.addView(mState.mFakeWindowRootView);
setContentView(mRootView);
@ -320,4 +334,24 @@ class SingleViewPresentation extends Presentation {
mFakeWindowRootView.updateViewLayout(view, layoutParams);
}
}
private static class AccessibilityDelegatingFrameLayout extends FrameLayout {
private final AccessibilityEventsDelegate mAccessibilityEventsDelegate;
private final View mEmbeddedView;
public AccessibilityDelegatingFrameLayout(
Context context,
AccessibilityEventsDelegate accessibilityEventsDelegate,
View ebeddedView
) {
super(context);
mAccessibilityEventsDelegate = accessibilityEventsDelegate;
mEmbeddedView = ebeddedView;
}
@Override
public boolean requestSendAccessibilityEvent(View child, AccessibilityEvent event) {
return mAccessibilityEventsDelegate.requestSendAccessibilityEvent(mEmbeddedView, child, event);
}
}
}

View File

@ -19,6 +19,7 @@ class VirtualDisplayController {
public static VirtualDisplayController create(
Context context,
AccessibilityEventsDelegate accessibilityEventsDelegate,
PlatformViewFactory viewFactory,
TextureRegistry.SurfaceTextureEntry textureEntry,
int width,
@ -45,10 +46,11 @@ class VirtualDisplayController {
}
return new VirtualDisplayController(
context, virtualDisplay, viewFactory, surface, textureEntry, viewId, createParams);
context, accessibilityEventsDelegate, virtualDisplay, viewFactory, surface, textureEntry, viewId, createParams);
}
private final Context mContext;
private final AccessibilityEventsDelegate mAccessibilityEventsDelegate;
private final int mDensityDpi;
private final TextureRegistry.SurfaceTextureEntry mTextureEntry;
private VirtualDisplay mVirtualDisplay;
@ -58,6 +60,7 @@ class VirtualDisplayController {
private VirtualDisplayController(
Context context,
AccessibilityEventsDelegate accessibilityEventsDelegate,
VirtualDisplay virtualDisplay,
PlatformViewFactory viewFactory,
Surface surface,
@ -65,13 +68,14 @@ class VirtualDisplayController {
int viewId,
Object createParams
) {
mContext = context;
mAccessibilityEventsDelegate = accessibilityEventsDelegate;
mTextureEntry = textureEntry;
mSurface = surface;
mContext = context;
mVirtualDisplay = virtualDisplay;
mDensityDpi = context.getResources().getDisplayMetrics().densityDpi;
mPresentation = new SingleViewPresentation(
context, mVirtualDisplay.getDisplay(), viewFactory, viewId, createParams);
context, mVirtualDisplay.getDisplay(), viewFactory, accessibilityEventsDelegate, viewId, createParams);
mPresentation.show();
}
@ -121,7 +125,7 @@ class VirtualDisplayController {
public void onViewDetachedFromWindow(View v) {}
});
mPresentation = new SingleViewPresentation(mContext, mVirtualDisplay.getDisplay(), presentationState);
mPresentation = new SingleViewPresentation(mContext, mVirtualDisplay.getDisplay(), mAccessibilityEventsDelegate, presentationState);
mPresentation.show();
}

View File

@ -5,9 +5,7 @@
package io.flutter.view;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.graphics.Rect;
import android.net.Uri;
@ -23,7 +21,6 @@ import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
@ -158,9 +155,23 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
// The {@code SemanticsNode} within Flutter that currently has the focus of Android's
// accessibility system.
//
// This is null when a node embedded by the AccessibilityViewEmbedder has the focus.
@Nullable
private SemanticsNode accessibilityFocusedSemanticsNode;
// The virtual ID of the currently embedded node with accessibility focus.
//
// This is the ID of a node generated by the AccessibilityViewEmbedder if an embedded node is focused,
// null otherwise.
private Integer embeddedAccessibilityFocusedNodeId;
// The virtual ID of the currently embedded node with input focus.
//
// This is the ID of a node generated by the AccessibilityViewEmbedder if an embedded node is focused,
// null otherwise.
private Integer embeddedInputFocusedNodeId;
// The accessibility features that should currently be active within Flutter, represented as
// a bitmask whose values comes from {@link AccessibilityFeature}.
private int accessibilityFeatureFlags = 0;
@ -762,6 +773,14 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
*/
@Override
public boolean performAction(int virtualViewId, int accessibilityAction, @Nullable Bundle arguments) {
if (virtualViewId >= MIN_ENGINE_GENERATED_NODE_ID) {
// The node is in the engine generated range, and is handled by the accessibility view embedder.
boolean didPerform = accessibilityViewEmbedder.performAction(virtualViewId, accessibilityAction, arguments);
if (didPerform && accessibilityAction == AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS) {
embeddedAccessibilityFocusedNodeId = null;
}
return didPerform;
}
SemanticsNode semanticsNode = flutterSemanticsTree.get(virtualViewId);
if (semanticsNode == null) {
return false;
@ -841,6 +860,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
);
accessibilityFocusedSemanticsNode = null;
embeddedAccessibilityFocusedNodeId = null;
return true;
}
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
@ -1014,12 +1034,18 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
if (inputFocusedSemanticsNode != null) {
return createAccessibilityNodeInfo(inputFocusedSemanticsNode.id);
}
if (embeddedInputFocusedNodeId != null) {
return createAccessibilityNodeInfo(embeddedInputFocusedNodeId);
}
}
// Fall through to check FOCUS_ACCESSIBILITY
case AccessibilityNodeInfo.FOCUS_ACCESSIBILITY: {
if (accessibilityFocusedSemanticsNode != null) {
return createAccessibilityNodeInfo(accessibilityFocusedSemanticsNode.id);
}
if (embeddedAccessibilityFocusedNodeId != null) {
return createAccessibilityNodeInfo(embeddedAccessibilityFocusedNodeId);
}
}
}
return null;
@ -1090,6 +1116,11 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
return false;
}
SemanticsNode semanticsNodeUnderCursor = getRootSemanticsNode().hitTest(new float[] {event.getX(), event.getY(), 0, 1});
if (semanticsNodeUnderCursor.platformViewId != -1) {
return accessibilityViewEmbedder.onAccessibilityHoverEvent(semanticsNodeUnderCursor.id, event);
}
if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER || event.getAction() == MotionEvent.ACTION_HOVER_MOVE) {
handleTouchExploration(event.getX(), event.getY());
} else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
@ -2061,4 +2092,46 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
return sb.length() > 0 ? sb.toString() : null;
}
}
/**
* Delegates handling of {@link android.view.ViewParent#requestSendAccessibilityEvent} to the accessibility bridge.
*
* This is used by embedded platform views to propagate accessibility events from their view hierarchy to the
* accessibility bridge.
*
* As the embedded view doesn't have to be the only View in the embedded hierarchy (it can have child views) and the
* event might have been originated from any view in this hierarchy, this method gets both a reference to the
* embedded platform view, and a reference to the view from its hierarchy that sent the event.
*
* @param embeddedView the embedded platform view for which the event is delegated
* @param eventOrigin the view in the embedded view's hierarchy that sent the event.
* @return True if the event was sent.
*/
public boolean externalViewRequestSendAccessibilityEvent(View embeddedView, View eventOrigin, AccessibilityEvent event) {
if (!accessibilityViewEmbedder.requestSendAccessibilityEvent(embeddedView, eventOrigin, event)){
return false;
}
Integer virtualNodeId = accessibilityViewEmbedder.getRecordFlutterId(embeddedView, event);
if (virtualNodeId == null) {
return false;
}
switch(event.getEventType()) {
case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
hoveredObject = null;
break;
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
embeddedAccessibilityFocusedNodeId = virtualNodeId;
accessibilityFocusedSemanticsNode = null;
break;
case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
embeddedInputFocusedNodeId = null;
embeddedAccessibilityFocusedNodeId = null;
break;
case AccessibilityEvent.TYPE_VIEW_FOCUSED:
embeddedInputFocusedNodeId = virtualNodeId;
inputFocusedSemanticsNode = null;
break;
}
return true;
}
}

View File

@ -6,11 +6,17 @@ package io.flutter.view;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
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 java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@ -53,7 +59,7 @@ class AccessibilityViewEmbedder {
private int nextFlutterId;
AccessibilityViewEmbedder(View rootAccessibiiltyView, int firstVirtualNodeId) {
AccessibilityViewEmbedder(@NonNull View rootAccessibiiltyView, int firstVirtualNodeId) {
reflectionAccessors = new ReflectionAccessors();
flutterIdToOrigin = new SparseArray<>();
this.rootAccessibilityView = rootAccessibiiltyView;
@ -68,24 +74,23 @@ class AccessibilityViewEmbedder {
* @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(View embeddedView, int flutterId, Rect displayBounds) {
return null;
// TODO(amirh): uncomment this once a11y events and actions are wired.
// AccessibilityNodeInfo originNode = embeddedView.createAccessibilityNodeInfo();
// Long originPackedId = reflectionAccessors.getSourceNodeId(originNode);
// if (originPackedId == null) {
// return null;
// }
// int originId = ReflectionAccessors.getVirtualNodeId(originPackedId);
// flutterIdToOrigin.put(flutterId, new ViewAndId(embeddedView, originId));
// flutterIdToDisplayBounds.put(flutterId, displayBounds);
// originToFlutterId.put(new ViewAndId(embeddedView, originId), flutterId);
// return convertToFlutterNode(originNode, flutterId, embeddedView);
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;
}
int originId = ReflectionAccessors.getVirtualNodeId(originPackedId);
flutterIdToOrigin.put(flutterId, new ViewAndId(embeddedView, originId));
flutterIdToDisplayBounds.put(flutterId, displayBounds);
originToFlutterId.put(new ViewAndId(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) {
@ -101,6 +106,9 @@ class AccessibilityViewEmbedder {
}
AccessibilityNodeInfo originNode =
origin.view.getAccessibilityNodeProvider().createAccessibilityNodeInfo(origin.id);
if (originNode == null) {
return null;
}
return convertToFlutterNode(originNode, flutterId, origin.view);
}
@ -108,7 +116,12 @@ class AccessibilityViewEmbedder {
* 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.
*/
private AccessibilityNodeInfo convertToFlutterNode(AccessibilityNodeInfo originNode, int flutterId, View embeddedView) {
@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);
@ -124,7 +137,11 @@ class AccessibilityViewEmbedder {
return result;
}
private void setFlutterNodeParent(AccessibilityNodeInfo originNode, View embeddedView, AccessibilityNodeInfo result) {
private void setFlutterNodeParent(
@NonNull AccessibilityNodeInfo originNode,
@NonNull View embeddedView,
@NonNull AccessibilityNodeInfo result
) {
Long parentOriginPackedId = reflectionAccessors.getParentNodeId(originNode);
if (parentOriginPackedId == null) {
return;
@ -137,7 +154,12 @@ class AccessibilityViewEmbedder {
}
private void addChildrenToFlutterNode(AccessibilityNodeInfo originNode, View embeddedView, Rect displayBounds, AccessibilityNodeInfo resultNode) {
private void addChildrenToFlutterNode(
@NonNull AccessibilityNodeInfo originNode,
@NonNull View embeddedView,
@NonNull Rect displayBounds,
@NonNull AccessibilityNodeInfo resultNode
) {
for (int i = 0; i < originNode.getChildCount(); i++) {
Long originPackedId = reflectionAccessors.getChildId(originNode, i);
if (originPackedId == null) {
@ -158,7 +180,11 @@ class AccessibilityViewEmbedder {
}
}
private void setFlutterNodesTranslateBounds(AccessibilityNodeInfo originNode, Rect displayBounds, AccessibilityNodeInfo resultNode) {
private void setFlutterNodesTranslateBounds(
@NonNull AccessibilityNodeInfo originNode,
@NonNull Rect displayBounds,
@NonNull AccessibilityNodeInfo resultNode
) {
Rect boundsInParent = new Rect();
originNode.getBoundsInParent(boundsInParent);
resultNode.setBoundsInParent(boundsInParent);
@ -169,7 +195,7 @@ class AccessibilityViewEmbedder {
resultNode.setBoundsInScreen(boundsInScreen);
}
private void copyAccessibilityFields(AccessibilityNodeInfo input, AccessibilityNodeInfo output) {
private void copyAccessibilityFields(@NonNull AccessibilityNodeInfo input, @NonNull AccessibilityNodeInfo output) {
output.setAccessibilityFocused(input.isAccessibilityFocused());
output.setCheckable(input.isCheckable());
output.setChecked(input.isChecked());
@ -220,6 +246,128 @@ class AccessibilityViewEmbedder {
}
}
/**
* 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) {
return false;
}
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.
*
* 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 = flutterIdToDisplayBounds.get(rootFlutterId);
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);
}
private static class ViewAndId {
final View view;
final int id;
@ -251,22 +399,29 @@ class AccessibilityViewEmbedder {
private static class ReflectionAccessors {
private final Method getSourceNodeId;
private final Method getParentNodeId;
private final Method getRecordSourceNodeId;
private final Method getChildId;
private ReflectionAccessors() {
Method getSourceNodeId = null;
Method getParentNodeId = null;
Method getRecordSourceNodeId = null;
Method getChildId = null;
try {
getSourceNodeId = AccessibilityNodeInfo.class.getMethod("getSourceNodeId");
} catch (NoSuchMethodException e) {
Log.w(TAG, "can't invoke getSourceNodeId with reflection");
Log.w(TAG, "can't invoke AccessibilityNodeInfo#getSourceNodeId with reflection");
}
try {
getParentNodeId = AccessibilityNodeInfo.class.getMethod("getParentNodeId");
} catch (NoSuchMethodException e) {
Log.w(TAG, "can't invoke getParentNodeId with reflection");
}
try {
getRecordSourceNodeId = AccessibilityRecord.class.getMethod("getSourceNodeId");
} catch (NoSuchMethodException e) {
Log.w(TAG, "can't invoke AccessibiiltyRecord#getSourceNodeId with reflection");
}
try {
getChildId = AccessibilityNodeInfo.class.getMethod("getChildId", int.class);
} catch (NoSuchMethodException e) {
@ -274,6 +429,7 @@ class AccessibilityViewEmbedder {
}
this.getSourceNodeId = getSourceNodeId;
this.getParentNodeId = getParentNodeId;
this.getRecordSourceNodeId = getRecordSourceNodeId;
this.getChildId = getChildId;
}
@ -282,7 +438,8 @@ class AccessibilityViewEmbedder {
return (int) (nodeId >> 32);
}
private Long getSourceNodeId(AccessibilityNodeInfo node) {
@Nullable
private Long getSourceNodeId(@NonNull AccessibilityNodeInfo node) {
if (getSourceNodeId == null) {
return null;
}
@ -296,7 +453,8 @@ class AccessibilityViewEmbedder {
return null;
}
private Long getChildId(AccessibilityNodeInfo node, int child) {
@Nullable
private Long getChildId(@NonNull AccessibilityNodeInfo node, int child) {
if (getChildId == null) {
return null;
}
@ -310,7 +468,8 @@ class AccessibilityViewEmbedder {
return null;
}
private Long getParentNodeId(AccessibilityNodeInfo node) {
@Nullable
private Long getParentNodeId(@NonNull AccessibilityNodeInfo node) {
if (getParentNodeId == null) {
return null;
}
@ -323,5 +482,20 @@ class AccessibilityViewEmbedder {
}
return null;
}
@Nullable
private Long getRecordSourceNodeId(@NonNull AccessibilityRecord node) {
if (getRecordSourceNodeId == null) {
return null;
}
try {
return (Long) getRecordSourceNodeId.invoke(node);
} catch (IllegalAccessException e) {
Log.w(TAG, e);
} catch (InvocationTargetException e) {
Log.w(TAG, e);
}
return null;
}
}
}