mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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:
parent
821bb52e38
commit
ab43a145cf
@ -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);
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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_;
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user