Skip to content

Commit

Permalink
Delegate a11y events and action to/from embedded Android platform vie…
Browse files Browse the repository at this point in the history
…ws. (flutter#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.
  • Loading branch information
amirh authored Mar 25, 2019
1 parent dd6be2f commit 345ae7d
Show file tree
Hide file tree
Showing 8 changed files with 376 additions and 38 deletions.
1 change: 1 addition & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions shell/platform/android/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -100,12 +101,12 @@ public void detach() {

@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() {
Expand Down Expand Up @@ -201,6 +202,7 @@ private void createPlatformView(MethodCall call, MethodChannel.Result result) {
TextureRegistry.SurfaceTextureEntry textureEntry = mTextureRegistry.createSurfaceTexture();
VirtualDisplayController vdController = VirtualDisplayController.create(
mContext,
mAccessibilityEventsDelegate,
viewFactory,
textureEntry,
toPhysicalPixels(logicalWidth),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import android.os.Bundle;
import android.util.Log;
import android.view.*;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;

import java.lang.reflect.*;
Expand Down Expand Up @@ -57,6 +58,9 @@ static class PresentationState {

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;
Expand All @@ -67,7 +71,7 @@ static class PresentationState {

// 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;
Expand All @@ -82,10 +86,13 @@ public SingleViewPresentation(
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();
Expand All @@ -102,8 +109,14 @@ public SingleViewPresentation(
* <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(
Expand All @@ -130,8 +143,9 @@ protected void onCreate(Bundle savedInstanceState) {
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);
Expand Down Expand Up @@ -320,4 +334,24 @@ private void updateViewLayout(Object[] args) {
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class VirtualDisplayController {

public static VirtualDisplayController create(
Context context,
AccessibilityEventsDelegate accessibilityEventsDelegate,
PlatformViewFactory viewFactory,
TextureRegistry.SurfaceTextureEntry textureEntry,
int width,
Expand All @@ -45,10 +46,11 @@ public static VirtualDisplayController create(
}

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;
Expand All @@ -58,20 +60,22 @@ public static VirtualDisplayController create(

private VirtualDisplayController(
Context context,
AccessibilityEventsDelegate accessibilityEventsDelegate,
VirtualDisplay virtualDisplay,
PlatformViewFactory viewFactory,
Surface surface,
TextureRegistry.SurfaceTextureEntry textureEntry,
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();
}

Expand Down Expand Up @@ -121,7 +125,7 @@ public void run() {
public void onViewDetachedFromWindow(View v) {}
});

mPresentation = new SingleViewPresentation(mContext, mVirtualDisplay.getDisplay(), presentationState);
mPresentation = new SingleViewPresentation(mContext, mVirtualDisplay.getDisplay(), mAccessibilityEventsDelegate, presentationState);
mPresentation.show();
}

Expand Down
79 changes: 76 additions & 3 deletions shell/platform/android/io/flutter/view/AccessibilityBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,7 +21,6 @@
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -762,6 +773,14 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
*/
@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;
Expand Down Expand Up @@ -841,6 +860,7 @@ public boolean performAction(int virtualViewId, int accessibilityAction, @Nullab
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
);
accessibilityFocusedSemanticsNode = null;
embeddedAccessibilityFocusedNodeId = null;
return true;
}
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
Expand Down Expand Up @@ -1014,12 +1034,18 @@ public AccessibilityNodeInfo findFocus(int focus) {
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;
Expand Down Expand Up @@ -1090,6 +1116,11 @@ public boolean onAccessibilityHoverEvent(MotionEvent event) {
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) {
Expand Down Expand Up @@ -2061,4 +2092,46 @@ private String getValueLabelHint() {
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;
}
}
Loading

0 comments on commit 345ae7d

Please sign in to comment.