Skip to content

Commit

Permalink
Handle a11y focus event on Ios and android (#41777)
Browse files Browse the repository at this point in the history
  • Loading branch information
hannah-hyj authored May 30, 2023
1 parent feb92f3 commit 1ba8091
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

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;
Expand All @@ -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(
Expand Down Expand Up @@ -67,6 +65,14 @@ public void onMessage(
}
break;
}
case "focus":
{
Integer nodeId = (Integer) annotatedEvent.get("nodeId");
if (nodeId != null) {
handler.onFocus(nodeId);
}
break;
}
case "tooltip":
{
String tooltipMessage = (String) data.get("message");
Expand Down Expand Up @@ -170,12 +176,15 @@ public interface AccessibilityMessageHandler extends FlutterJNI.AccessibilityDel
/** 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);
}
Expand Down
16 changes: 14 additions & 2 deletions shell/platform/android/io/flutter/view/AccessibilityBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,12 @@ public void onLongPress(int nodeId) {
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) {
Expand Down Expand Up @@ -1883,7 +1889,8 @@ private AccessibilityEvent createTextChangedEvent(int id, String oldValue, Strin
* <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;
}
Expand Down Expand Up @@ -1976,12 +1983,17 @@ private void sendWindowContentChangeEvent(int virtualViewId) {
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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;
Expand All @@ -30,4 +31,19 @@ public void repliesWhenNoAccessibilityHandler() throws JSONException {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,16 @@
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;
Expand Down Expand Up @@ -1827,6 +1831,66 @@ public void releaseDropsChannelMessageHandler() {
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@ static bool DidFlagChange(const flutter::SemanticsNode& oldNode,
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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1289,6 +1289,50 @@ - (void)testAnnouncesRouteChangesRemoveRouteInMiddle {
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");
Expand Down

0 comments on commit 1ba8091

Please sign in to comment.