diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 567ad251512..0a30b3ca651 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -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 diff --git a/engine/src/flutter/shell/platform/android/BUILD.gn b/engine/src/flutter/shell/platform/android/BUILD.gn index 4244e7946bb..090fdb28ff6 100644 --- a/engine/src/flutter/shell/platform/android/BUILD.gn +++ b/engine/src/flutter/shell/platform/android/BUILD.gn @@ -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", diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/AccessibilityEventsDelegate.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/AccessibilityEventsDelegate.java new file mode 100644 index 00000000000..1a3022cfc9c --- /dev/null +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/AccessibilityEventsDelegate.java @@ -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; + } +} diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java index f19fabd317e..5470e03a9f2 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java @@ -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 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), diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java index 24675bbbf8a..a0cbde42b54 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/SingleViewPresentation.java @@ -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 { *

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); + } + } } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java index 9985361762b..876e6be5ced 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/VirtualDisplayController.java @@ -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(); } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 7a798d3b37b..a589f54d4ca 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -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; + } } diff --git a/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java b/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java index 1e5da786321..330ded624dd 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityViewEmbedder.java @@ -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; + } } }