Handle a11y focus event on Ios and android (flutter/engine#41777)

framework change:https://github.com/flutter/flutter/pull/126171
issue: https://github.com/flutter/flutter/issues/94523

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
hangyu 2023-05-30 14:50:56 -07:00 committed by GitHub
parent 821bb52e38
commit ab43a145cf
7 changed files with 157 additions and 8 deletions

View File

@ -2,7 +2,6 @@ package io.flutter.embedding.engine.systemchannels;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.dart.DartExecutor;
@ -24,8 +23,7 @@ public class AccessibilityChannel {
@NonNull public final FlutterJNI flutterJNI;
@Nullable private AccessibilityMessageHandler handler;
@VisibleForTesting
final BasicMessageChannel.MessageHandler<Object> parsingMessageHandler =
public final BasicMessageChannel.MessageHandler<Object> parsingMessageHandler =
new BasicMessageChannel.MessageHandler<Object>() {
@Override
public void onMessage(
@ -67,6 +65,14 @@ public class AccessibilityChannel {
}
break;
}
case "focus":
{
Integer nodeId = (Integer) annotatedEvent.get("nodeId");
if (nodeId != null) {
handler.onFocus(nodeId);
}
break;
}
case "tooltip":
{
String tooltipMessage = (String) data.get("message");
@ -170,12 +176,15 @@ public class AccessibilityChannel {
/** The Dart application would like the given {@code message} to be announced. */
void announce(@NonNull String message);
/** The user has tapped on the widget with the given {@code nodeId}. */
/** The user has tapped on the semantics node with the given {@code nodeId}. */
void onTap(int nodeId);
/** The user has long pressed on the widget with the given {@code nodeId}. */
/** The user has long pressed on the semantics node with the given {@code nodeId}. */
void onLongPress(int nodeId);
/** The framework has requested focus on the semantics node with the given {@code nodeId}. */
void onFocus(int nodeId);
/** The user has opened a tooltip. */
void onTooltip(@NonNull String message);
}

View File

@ -309,6 +309,12 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
}
/** The framework has requested focus on the given {@code nodeId}. */
@Override
public void onFocus(int nodeId) {
sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_FOCUSED);
}
/** The user has opened a tooltip. */
@Override
public void onTooltip(@NonNull String message) {
@ -1883,7 +1889,8 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
* <p>The given {@code viewId} may either belong to {@link #rootAccessibilityView}, or any Flutter
* {@link SemanticsNode}.
*/
private void sendAccessibilityEvent(int viewId, int eventType) {
@VisibleForTesting
public void sendAccessibilityEvent(int viewId, int eventType) {
if (!accessibilityManager.isEnabled()) {
return;
}
@ -1976,12 +1983,17 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
* invoked to create an {@link AccessibilityEvent} for the {@link #rootAccessibilityView}.
*/
private AccessibilityEvent obtainAccessibilityEvent(int virtualViewId, int eventType) {
AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
AccessibilityEvent event = obtainAccessibilityEvent(eventType);
event.setPackageName(rootAccessibilityView.getContext().getPackageName());
event.setSource(rootAccessibilityView, virtualViewId);
return event;
}
@VisibleForTesting
public AccessibilityEvent obtainAccessibilityEvent(int eventType) {
return AccessibilityEvent.obtain(eventType);
}
/**
* Reads the {@code layoutInDisplayCutoutMode} value from the window attribute and returns whether
* a left cutout inset is required.

View File

@ -7,6 +7,7 @@ import android.annotation.TargetApi;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.plugin.common.BasicMessageChannel;
import java.util.HashMap;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Test;
@ -30,4 +31,19 @@ public class AccessibilityChannelTest {
accessibilityChannel.parsingMessageHandler.onMessage(arguments, reply);
verify(reply).reply(null);
}
@Test
public void handleFocus() throws JSONException {
AccessibilityChannel accessibilityChannel =
new AccessibilityChannel(mock(DartExecutor.class), mock(FlutterJNI.class));
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("type", "focus");
arguments.put("nodeId", 123);
AccessibilityChannel.AccessibilityMessageHandler handler =
mock(AccessibilityChannel.AccessibilityMessageHandler.class);
accessibilityChannel.setAccessibilityMessageHandler(handler);
BasicMessageChannel.Reply reply = mock(BasicMessageChannel.Reply.class);
accessibilityChannel.parsingMessageHandler.onMessage(arguments, reply);
verify(handler).onFocus(123);
}
}

View File

@ -44,12 +44,16 @@ import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.plugin.common.BasicMessageChannel;
import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate;
import io.flutter.view.AccessibilityBridge.Flag;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -1827,6 +1831,66 @@ public class AccessibilityBridgeTest {
verify(mockChannel, never()).setAccessibilityFeatures(anyInt());
}
@Test
public void sendFocusAccessibilityEvent() {
AccessibilityManager mockManager = mock(AccessibilityManager.class);
AccessibilityChannel accessibilityChannel =
new AccessibilityChannel(mock(DartExecutor.class), mock(FlutterJNI.class));
ContentResolver mockContentResolver = mock(ContentResolver.class);
View mockRootView = mock(View.class);
Context context = mock(Context.class);
when(mockRootView.getContext()).thenReturn(context);
when(context.getPackageName()).thenReturn("test");
ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);
AccessibilityBridge accessibilityBridge =
setUpBridge(mockRootView, accessibilityChannel, mockManager, null, null, null);
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("type", "focus");
arguments.put("nodeId", 123);
BasicMessageChannel.Reply reply = mock(BasicMessageChannel.Reply.class);
accessibilityChannel.parsingMessageHandler.onMessage(arguments, reply);
// Check that focus event was sent.
ArgumentCaptor<AccessibilityEvent> eventCaptor =
ArgumentCaptor.forClass(AccessibilityEvent.class);
verify(mockParent).requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
AccessibilityEvent event = eventCaptor.getAllValues().get(0);
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_FOCUSED);
assertEquals(event.getSource(), null);
}
@Test
public void SetSourceAndPackageNameForAccessibilityEvent() {
AccessibilityManager mockManager = mock(AccessibilityManager.class);
ContentResolver mockContentResolver = mock(ContentResolver.class);
View mockRootView = mock(View.class);
Context context = mock(Context.class);
when(mockRootView.getContext()).thenReturn(context);
when(context.getPackageName()).thenReturn("test");
when(mockManager.isEnabled()).thenReturn(true);
ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
AccessibilityEvent mockEvent = mock(AccessibilityEvent.class);
AccessibilityBridge accessibilityBridge =
setUpBridge(mockRootView, null, mockManager, null, null, null);
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
when(spyAccessibilityBridge.obtainAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED))
.thenReturn(mockEvent);
spyAccessibilityBridge.sendAccessibilityEvent(123, AccessibilityEvent.TYPE_VIEW_FOCUSED);
verify(mockEvent).setPackageName("test");
verify(mockEvent).setSource(eq(mockRootView), eq(123));
}
AccessibilityBridge setUpBridge() {
return setUpBridge(null, null, null, null, null, null);
}

View File

@ -57,6 +57,7 @@ class AccessibilityBridge final : public AccessibilityBridgeIos {
void UpdateSemantics(flutter::SemanticsNodeUpdates nodes,
const flutter::CustomAccessibilityActionUpdates& actions);
void HandleEvent(NSDictionary<NSString*, id>* annotatedEvent);
void DispatchSemanticsAction(int32_t id, flutter::SemanticsAction action) override;
void DispatchSemanticsAction(int32_t id,
flutter::SemanticsAction action,
@ -88,7 +89,6 @@ class AccessibilityBridge final : public AccessibilityBridgeIos {
SemanticsObject* FindFirstFocusable(SemanticsObject* parent);
void VisitObjectsRecursivelyAndRemove(SemanticsObject* object,
NSMutableArray<NSNumber*>* doomed_uids);
void HandleEvent(NSDictionary<NSString*, id>* annotatedEvent);
FlutterViewController* view_controller_;
PlatformViewIOS* platform_view_;

View File

@ -362,6 +362,10 @@ void AccessibilityBridge::HandleEvent(NSDictionary<NSString*, id>* annotatedEven
NSString* message = annotatedEvent[@"data"][@"message"];
ios_delegate_->PostAccessibilityNotification(UIAccessibilityAnnouncementNotification, message);
}
if ([type isEqualToString:@"focus"]) {
SemanticsObject* node = objects_.get()[annotatedEvent[@"nodeId"]];
ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, node);
}
}
fml::WeakPtr<AccessibilityBridge> AccessibilityBridge::GetWeakPtr() {

View File

@ -1289,6 +1289,50 @@ fml::RefPtr<fml::TaskRunner> CreateNewThread(std::string name) {
UIAccessibilityScreenChangedNotification);
}
- (void)testHandleEvent {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/nil,
/*task_runners=*/runners,
/*worker_task_runner=*/nil,
/*is_gpu_disabled_sync_switch=*/nil);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
NSDictionary<NSString*, id>* annotatedEvent = @{@"type" : @"focus", @"nodeId" : @123};
bridge->HandleEvent(annotatedEvent);
XCTAssertEqual([accessibility_notifications count], 1ul);
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityLayoutChangedNotification);
}
- (void)testAnnouncesRouteChangesWhenNoNamesRoute {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");