未验证 提交 f4d6ce13 编写于 作者: M Michael Goderbauer 提交者: GitHub

Clear focus if a platform view goes away (#17381)

上级 ef161fb5
...@@ -26,6 +26,7 @@ import android.view.accessibility.AccessibilityNodeProvider; ...@@ -26,6 +26,7 @@ import android.view.accessibility.AccessibilityNodeProvider;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import io.flutter.BuildConfig; import io.flutter.BuildConfig;
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate; import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate;
...@@ -333,10 +334,32 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { ...@@ -333,10 +334,32 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
// TODO(mattcarrol): Add the annotation once the plumbing is done. // TODO(mattcarrol): Add the annotation once the plumbing is done.
// https://github.com/flutter/flutter/issues/29618 // https://github.com/flutter/flutter/issues/29618
PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) { PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) {
this(
rootAccessibilityView,
accessibilityChannel,
accessibilityManager,
contentResolver,
new AccessibilityViewEmbedder(rootAccessibilityView, MIN_ENGINE_GENERATED_NODE_ID),
platformViewsAccessibilityDelegate);
}
@VisibleForTesting
public AccessibilityBridge(
@NonNull View rootAccessibilityView,
@NonNull AccessibilityChannel accessibilityChannel,
@NonNull AccessibilityManager accessibilityManager,
@NonNull ContentResolver contentResolver,
@NonNull AccessibilityViewEmbedder accessibilityViewEmbedder,
// This should be @NonNull once the plumbing for
// io.flutter.embedding.engine.android.FlutterView is done.
// TODO(mattcarrol): Add the annotation once the plumbing is done.
// https://github.com/flutter/flutter/issues/29618
PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) {
this.rootAccessibilityView = rootAccessibilityView; this.rootAccessibilityView = rootAccessibilityView;
this.accessibilityChannel = accessibilityChannel; this.accessibilityChannel = accessibilityChannel;
this.accessibilityManager = accessibilityManager; this.accessibilityManager = accessibilityManager;
this.contentResolver = contentResolver; this.contentResolver = contentResolver;
this.accessibilityViewEmbedder = accessibilityViewEmbedder;
this.platformViewsAccessibilityDelegate = platformViewsAccessibilityDelegate; this.platformViewsAccessibilityDelegate = platformViewsAccessibilityDelegate;
// Tell Flutter whether accessibility is initially active or not. Then register a listener // Tell Flutter whether accessibility is initially active or not. Then register a listener
...@@ -388,8 +411,6 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { ...@@ -388,8 +411,6 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
if (platformViewsAccessibilityDelegate != null) { if (platformViewsAccessibilityDelegate != null) {
platformViewsAccessibilityDelegate.attachAccessibilityBridge(this); platformViewsAccessibilityDelegate.attachAccessibilityBridge(this);
} }
accessibilityViewEmbedder =
new AccessibilityViewEmbedder(rootAccessibilityView, MIN_ENGINE_GENERATED_NODE_ID);
} }
/** /**
...@@ -1580,15 +1601,31 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { ...@@ -1580,15 +1601,31 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
// for null'ing accessibilityFocusedSemanticsNode, inputFocusedSemanticsNode, // for null'ing accessibilityFocusedSemanticsNode, inputFocusedSemanticsNode,
// and hoveredObject. Is this a hook method or a command? // and hoveredObject. Is this a hook method or a command?
semanticsNodeToBeRemoved.parent = null; semanticsNodeToBeRemoved.parent = null;
if (semanticsNodeToBeRemoved.platformViewId != -1
&& embeddedAccessibilityFocusedNodeId != null
&& accessibilityViewEmbedder.platformViewOfNode(embeddedAccessibilityFocusedNodeId)
== platformViewsAccessibilityDelegate.getPlatformViewById(
semanticsNodeToBeRemoved.platformViewId)) {
// If the currently focused a11y node is within a platform view that is
// getting removed: clear it's a11y focus.
sendAccessibilityEvent(
embeddedAccessibilityFocusedNodeId,
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
embeddedAccessibilityFocusedNodeId = null;
}
if (accessibilityFocusedSemanticsNode == semanticsNodeToBeRemoved) { if (accessibilityFocusedSemanticsNode == semanticsNodeToBeRemoved) {
sendAccessibilityEvent( sendAccessibilityEvent(
accessibilityFocusedSemanticsNode.id, accessibilityFocusedSemanticsNode.id,
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
accessibilityFocusedSemanticsNode = null; accessibilityFocusedSemanticsNode = null;
} }
if (inputFocusedSemanticsNode == semanticsNodeToBeRemoved) { if (inputFocusedSemanticsNode == semanticsNodeToBeRemoved) {
inputFocusedSemanticsNode = null; inputFocusedSemanticsNode = null;
} }
if (hoveredObject == semanticsNodeToBeRemoved) { if (hoveredObject == semanticsNodeToBeRemoved) {
hoveredObject = null; hoveredObject = null;
} }
......
...@@ -44,7 +44,7 @@ import java.util.Map; ...@@ -44,7 +44,7 @@ import java.util.Map;
* corresponding platform view and `originId`. * corresponding platform view and `originId`.
*/ */
@Keep @Keep
final class AccessibilityViewEmbedder { class AccessibilityViewEmbedder {
private static final String TAG = "AccessibilityBridge"; private static final String TAG = "AccessibilityBridge";
private final ReflectionAccessors reflectionAccessors; private final ReflectionAccessors reflectionAccessors;
...@@ -387,6 +387,18 @@ final class AccessibilityViewEmbedder { ...@@ -387,6 +387,18 @@ final class AccessibilityViewEmbedder {
return origin.view.dispatchGenericMotionEvent(translatedEvent); return origin.view.dispatchGenericMotionEvent(translatedEvent);
} }
/**
* Returns the View that contains the accessibility node identified by the provided flutterId or
* null if it doesn't belong to a view.
*/
public View platformViewOfNode(int flutterId) {
ViewAndId viewAndId = flutterIdToOrigin.get(flutterId);
if (viewAndId == null) {
return null;
}
return viewAndId.view;
}
private static class ViewAndId { private static class ViewAndId {
final View view; final View view;
final int id; final int id;
......
...@@ -5,20 +5,27 @@ ...@@ -5,20 +5,27 @@
package io.flutter.view; package io.flutter.view;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.view.View; import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo;
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate; import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
...@@ -73,24 +80,103 @@ public class AccessibilityBridgeTest { ...@@ -73,24 +80,103 @@ public class AccessibilityBridgeTest {
assertEquals(nodeInfo.getText(), null); assertEquals(nodeInfo.getText(), null);
} }
AccessibilityBridge setUpBridge() { @Test
View view = mock(View.class); public void itUnfocusesPlatformViewWhenPlatformViewGoesAway() {
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
AccessibilityManager mockManager = mock(AccessibilityManager.class);
View mockRootView = mock(View.class);
Context context = mock(Context.class); Context context = mock(Context.class);
when(view.getContext()).thenReturn(context); when(mockRootView.getContext()).thenReturn(context);
when(context.getPackageName()).thenReturn("test"); when(context.getPackageName()).thenReturn("test");
AccessibilityChannel accessibilityChannel = mock(AccessibilityChannel.class);
AccessibilityManager accessibilityManager = mock(AccessibilityManager.class);
ContentResolver contentResolver = mock(ContentResolver.class);
PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate =
mock(PlatformViewsAccessibilityDelegate.class);
AccessibilityBridge accessibilityBridge = AccessibilityBridge accessibilityBridge =
new AccessibilityBridge( setUpBridge(mockRootView, mockManager, mockViewEmbedder);
view,
accessibilityChannel, // Sent a11y tree with platform view.
accessibilityManager, TestSemanticsNode root = new TestSemanticsNode();
contentResolver, root.id = 0;
platformViewsAccessibilityDelegate); TestSemanticsNode platformView = new TestSemanticsNode();
return accessibilityBridge; platformView.id = 1;
platformView.platformViewId = 42;
root.children.add(platformView);
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
// Set a11y focus to platform view.
View mockView = mock(View.class);
AccessibilityEvent focusEvent = mock(AccessibilityEvent.class);
when(mockViewEmbedder.requestSendAccessibilityEvent(mockView, mockView, focusEvent))
.thenReturn(true);
when(mockViewEmbedder.getRecordFlutterId(mockView, focusEvent)).thenReturn(42);
when(focusEvent.getEventType()).thenReturn(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
accessibilityBridge.externalViewRequestSendAccessibilityEvent(mockView, mockView, focusEvent);
// Replace the platform view.
TestSemanticsNode node = new TestSemanticsNode();
node.id = 2;
root.children.clear();
root.children.add(node);
testSemanticsUpdate = root.toUpdate();
when(mockManager.isEnabled()).thenReturn(true);
ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
// Check that unfocus event was sent.
ArgumentCaptor<AccessibilityEvent> eventCaptor =
ArgumentCaptor.forClass(AccessibilityEvent.class);
verify(mockParent, times(2))
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
AccessibilityEvent event = eventCaptor.getAllValues().get(0);
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
}
AccessibilityBridge setUpBridge() {
return setUpBridge(null, null, null, null, null, null);
}
AccessibilityBridge setUpBridge(
View rootAccessibilityView,
AccessibilityManager accessibilityManager,
AccessibilityViewEmbedder accessibilityViewEmbedder) {
return setUpBridge(
rootAccessibilityView, null, accessibilityManager, null, accessibilityViewEmbedder, null);
}
AccessibilityBridge setUpBridge(
View rootAccessibilityView,
AccessibilityChannel accessibilityChannel,
AccessibilityManager accessibilityManager,
ContentResolver contentResolver,
AccessibilityViewEmbedder accessibilityViewEmbedder,
PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) {
if (rootAccessibilityView == null) {
rootAccessibilityView = mock(View.class);
Context context = mock(Context.class);
when(rootAccessibilityView.getContext()).thenReturn(context);
when(context.getPackageName()).thenReturn("test");
}
if (accessibilityChannel == null) {
accessibilityChannel = mock(AccessibilityChannel.class);
}
if (accessibilityManager == null) {
accessibilityManager = mock(AccessibilityManager.class);
}
if (contentResolver == null) {
contentResolver = mock(ContentResolver.class);
}
if (accessibilityViewEmbedder == null) {
accessibilityViewEmbedder = mock(AccessibilityViewEmbedder.class);
}
if (platformViewsAccessibilityDelegate == null) {
platformViewsAccessibilityDelegate = mock(PlatformViewsAccessibilityDelegate.class);
}
return new AccessibilityBridge(
rootAccessibilityView,
accessibilityChannel,
accessibilityManager,
contentResolver,
accessibilityViewEmbedder,
platformViewsAccessibilityDelegate);
} }
/// The encoding for semantics is described in platform_view_android.cc /// The encoding for semantics is described in platform_view_android.cc
...@@ -136,11 +222,18 @@ public class AccessibilityBridgeTest { ...@@ -136,11 +222,18 @@ public class AccessibilityBridgeTest {
float top = 0.0f; float top = 0.0f;
float right = 0.0f; float right = 0.0f;
float bottom = 0.0f; float bottom = 0.0f;
// children and custom actions not supported. final List<TestSemanticsNode> children = new ArrayList<TestSemanticsNode>();
// custom actions not supported.
TestSemanticsUpdate toUpdate() { TestSemanticsUpdate toUpdate() {
ArrayList<String> strings = new ArrayList<String>(); ArrayList<String> strings = new ArrayList<String>();
ByteBuffer bytes = ByteBuffer.allocate(1000); ByteBuffer bytes = ByteBuffer.allocate(1000);
addToBuffer(bytes, strings);
bytes.flip();
return new TestSemanticsUpdate(bytes, strings.toArray(new String[strings.size()]));
}
protected void addToBuffer(ByteBuffer bytes, ArrayList<String> strings) {
bytes.putInt(id); bytes.putInt(id);
bytes.putInt(flags); bytes.putInt(flags);
bytes.putInt(actions); bytes.putInt(actions);
...@@ -169,11 +262,20 @@ public class AccessibilityBridgeTest { ...@@ -169,11 +262,20 @@ public class AccessibilityBridgeTest {
bytes.putFloat(0); bytes.putFloat(0);
} }
// children in traversal order. // children in traversal order.
bytes.putInt(0); bytes.putInt(children.size());
for (TestSemanticsNode node : children) {
bytes.putInt(node.id);
}
// children in hit test order.
for (TestSemanticsNode node : children) {
bytes.putInt(node.id);
}
// custom actions // custom actions
bytes.putInt(0); bytes.putInt(0);
bytes.flip(); // child nodes
return new TestSemanticsUpdate(bytes, strings.toArray(new String[strings.size()])); for (TestSemanticsNode node : children) {
node.addToBuffer(bytes, strings);
}
} }
} }
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册