Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 1ba8091

Browse files
authored
Handle a11y focus event on Ios and android (#41777)
framework change:flutter/flutter#126171 issue: flutter/flutter#94523 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent feb92f3 commit 1ba8091

File tree

7 files changed

+157
-8
lines changed

7 files changed

+157
-8
lines changed

shell/platform/android/io/flutter/embedding/engine/systemchannels/AccessibilityChannel.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import androidx.annotation.NonNull;
44
import androidx.annotation.Nullable;
5-
import androidx.annotation.VisibleForTesting;
65
import io.flutter.Log;
76
import io.flutter.embedding.engine.FlutterJNI;
87
import io.flutter.embedding.engine.dart.DartExecutor;
@@ -24,8 +23,7 @@ public class AccessibilityChannel {
2423
@NonNull public final FlutterJNI flutterJNI;
2524
@Nullable private AccessibilityMessageHandler handler;
2625

27-
@VisibleForTesting
28-
final BasicMessageChannel.MessageHandler<Object> parsingMessageHandler =
26+
public final BasicMessageChannel.MessageHandler<Object> parsingMessageHandler =
2927
new BasicMessageChannel.MessageHandler<Object>() {
3028
@Override
3129
public void onMessage(
@@ -67,6 +65,14 @@ public void onMessage(
6765
}
6866
break;
6967
}
68+
case "focus":
69+
{
70+
Integer nodeId = (Integer) annotatedEvent.get("nodeId");
71+
if (nodeId != null) {
72+
handler.onFocus(nodeId);
73+
}
74+
break;
75+
}
7076
case "tooltip":
7177
{
7278
String tooltipMessage = (String) data.get("message");
@@ -170,12 +176,15 @@ public interface AccessibilityMessageHandler extends FlutterJNI.AccessibilityDel
170176
/** The Dart application would like the given {@code message} to be announced. */
171177
void announce(@NonNull String message);
172178

173-
/** The user has tapped on the widget with the given {@code nodeId}. */
179+
/** The user has tapped on the semantics node with the given {@code nodeId}. */
174180
void onTap(int nodeId);
175181

176-
/** The user has long pressed on the widget with the given {@code nodeId}. */
182+
/** The user has long pressed on the semantics node with the given {@code nodeId}. */
177183
void onLongPress(int nodeId);
178184

185+
/** The framework has requested focus on the semantics node with the given {@code nodeId}. */
186+
void onFocus(int nodeId);
187+
179188
/** The user has opened a tooltip. */
180189
void onTooltip(@NonNull String message);
181190
}

shell/platform/android/io/flutter/view/AccessibilityBridge.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,12 @@ public void onLongPress(int nodeId) {
309309
sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
310310
}
311311

312+
/** The framework has requested focus on the given {@code nodeId}. */
313+
@Override
314+
public void onFocus(int nodeId) {
315+
sendAccessibilityEvent(nodeId, AccessibilityEvent.TYPE_VIEW_FOCUSED);
316+
}
317+
312318
/** The user has opened a tooltip. */
313319
@Override
314320
public void onTooltip(@NonNull String message) {
@@ -1883,7 +1889,8 @@ private AccessibilityEvent createTextChangedEvent(int id, String oldValue, Strin
18831889
* <p>The given {@code viewId} may either belong to {@link #rootAccessibilityView}, or any Flutter
18841890
* {@link SemanticsNode}.
18851891
*/
1886-
private void sendAccessibilityEvent(int viewId, int eventType) {
1892+
@VisibleForTesting
1893+
public void sendAccessibilityEvent(int viewId, int eventType) {
18871894
if (!accessibilityManager.isEnabled()) {
18881895
return;
18891896
}
@@ -1976,12 +1983,17 @@ private void sendWindowContentChangeEvent(int virtualViewId) {
19761983
* invoked to create an {@link AccessibilityEvent} for the {@link #rootAccessibilityView}.
19771984
*/
19781985
private AccessibilityEvent obtainAccessibilityEvent(int virtualViewId, int eventType) {
1979-
AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
1986+
AccessibilityEvent event = obtainAccessibilityEvent(eventType);
19801987
event.setPackageName(rootAccessibilityView.getContext().getPackageName());
19811988
event.setSource(rootAccessibilityView, virtualViewId);
19821989
return event;
19831990
}
19841991

1992+
@VisibleForTesting
1993+
public AccessibilityEvent obtainAccessibilityEvent(int eventType) {
1994+
return AccessibilityEvent.obtain(eventType);
1995+
}
1996+
19851997
/**
19861998
* Reads the {@code layoutInDisplayCutoutMode} value from the window attribute and returns whether
19871999
* a left cutout inset is required.

shell/platform/android/test/io/flutter/embedding/engine/systemchannels/AccessibilityChannelTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.flutter.embedding.engine.FlutterJNI;
88
import io.flutter.embedding.engine.dart.DartExecutor;
99
import io.flutter.plugin.common.BasicMessageChannel;
10+
import java.util.HashMap;
1011
import org.json.JSONException;
1112
import org.json.JSONObject;
1213
import org.junit.Test;
@@ -30,4 +31,19 @@ public void repliesWhenNoAccessibilityHandler() throws JSONException {
3031
accessibilityChannel.parsingMessageHandler.onMessage(arguments, reply);
3132
verify(reply).reply(null);
3233
}
34+
35+
@Test
36+
public void handleFocus() throws JSONException {
37+
AccessibilityChannel accessibilityChannel =
38+
new AccessibilityChannel(mock(DartExecutor.class), mock(FlutterJNI.class));
39+
HashMap<String, Object> arguments = new HashMap<>();
40+
arguments.put("type", "focus");
41+
arguments.put("nodeId", 123);
42+
AccessibilityChannel.AccessibilityMessageHandler handler =
43+
mock(AccessibilityChannel.AccessibilityMessageHandler.class);
44+
accessibilityChannel.setAccessibilityMessageHandler(handler);
45+
BasicMessageChannel.Reply reply = mock(BasicMessageChannel.Reply.class);
46+
accessibilityChannel.parsingMessageHandler.onMessage(arguments, reply);
47+
verify(handler).onFocus(123);
48+
}
3349
}

shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,16 @@
4444
import android.view.accessibility.AccessibilityNodeInfo;
4545
import androidx.test.core.app.ApplicationProvider;
4646
import androidx.test.ext.junit.runners.AndroidJUnit4;
47+
import io.flutter.embedding.engine.FlutterJNI;
48+
import io.flutter.embedding.engine.dart.DartExecutor;
4749
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
50+
import io.flutter.plugin.common.BasicMessageChannel;
4851
import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate;
4952
import io.flutter.view.AccessibilityBridge.Flag;
5053
import java.nio.ByteBuffer;
5154
import java.nio.charset.Charset;
5255
import java.util.ArrayList;
56+
import java.util.HashMap;
5357
import java.util.List;
5458
import org.junit.Test;
5559
import org.junit.runner.RunWith;
@@ -1827,6 +1831,66 @@ public void releaseDropsChannelMessageHandler() {
18271831
verify(mockChannel, never()).setAccessibilityFeatures(anyInt());
18281832
}
18291833

1834+
@Test
1835+
public void sendFocusAccessibilityEvent() {
1836+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
1837+
AccessibilityChannel accessibilityChannel =
1838+
new AccessibilityChannel(mock(DartExecutor.class), mock(FlutterJNI.class));
1839+
1840+
ContentResolver mockContentResolver = mock(ContentResolver.class);
1841+
View mockRootView = mock(View.class);
1842+
Context context = mock(Context.class);
1843+
when(mockRootView.getContext()).thenReturn(context);
1844+
when(context.getPackageName()).thenReturn("test");
1845+
ViewParent mockParent = mock(ViewParent.class);
1846+
when(mockRootView.getParent()).thenReturn(mockParent);
1847+
when(mockManager.isEnabled()).thenReturn(true);
1848+
1849+
AccessibilityBridge accessibilityBridge =
1850+
setUpBridge(mockRootView, accessibilityChannel, mockManager, null, null, null);
1851+
1852+
HashMap<String, Object> arguments = new HashMap<>();
1853+
arguments.put("type", "focus");
1854+
arguments.put("nodeId", 123);
1855+
BasicMessageChannel.Reply reply = mock(BasicMessageChannel.Reply.class);
1856+
accessibilityChannel.parsingMessageHandler.onMessage(arguments, reply);
1857+
1858+
// Check that focus event was sent.
1859+
ArgumentCaptor<AccessibilityEvent> eventCaptor =
1860+
ArgumentCaptor.forClass(AccessibilityEvent.class);
1861+
verify(mockParent).requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
1862+
AccessibilityEvent event = eventCaptor.getAllValues().get(0);
1863+
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_FOCUSED);
1864+
assertEquals(event.getSource(), null);
1865+
}
1866+
1867+
@Test
1868+
public void SetSourceAndPackageNameForAccessibilityEvent() {
1869+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
1870+
ContentResolver mockContentResolver = mock(ContentResolver.class);
1871+
View mockRootView = mock(View.class);
1872+
Context context = mock(Context.class);
1873+
when(mockRootView.getContext()).thenReturn(context);
1874+
when(context.getPackageName()).thenReturn("test");
1875+
when(mockManager.isEnabled()).thenReturn(true);
1876+
ViewParent mockParent = mock(ViewParent.class);
1877+
when(mockRootView.getParent()).thenReturn(mockParent);
1878+
AccessibilityEvent mockEvent = mock(AccessibilityEvent.class);
1879+
1880+
AccessibilityBridge accessibilityBridge =
1881+
setUpBridge(mockRootView, null, mockManager, null, null, null);
1882+
1883+
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
1884+
1885+
when(spyAccessibilityBridge.obtainAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED))
1886+
.thenReturn(mockEvent);
1887+
1888+
spyAccessibilityBridge.sendAccessibilityEvent(123, AccessibilityEvent.TYPE_VIEW_FOCUSED);
1889+
1890+
verify(mockEvent).setPackageName("test");
1891+
verify(mockEvent).setSource(eq(mockRootView), eq(123));
1892+
}
1893+
18301894
AccessibilityBridge setUpBridge() {
18311895
return setUpBridge(null, null, null, null, null, null);
18321896
}

shell/platform/darwin/ios/framework/Source/accessibility_bridge.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ class AccessibilityBridge final : public AccessibilityBridgeIos {
5757

5858
void UpdateSemantics(flutter::SemanticsNodeUpdates nodes,
5959
const flutter::CustomAccessibilityActionUpdates& actions);
60+
void HandleEvent(NSDictionary<NSString*, id>* annotatedEvent);
6061
void DispatchSemanticsAction(int32_t id, flutter::SemanticsAction action) override;
6162
void DispatchSemanticsAction(int32_t id,
6263
flutter::SemanticsAction action,
@@ -88,7 +89,6 @@ class AccessibilityBridge final : public AccessibilityBridgeIos {
8889
SemanticsObject* FindFirstFocusable(SemanticsObject* parent);
8990
void VisitObjectsRecursivelyAndRemove(SemanticsObject* object,
9091
NSMutableArray<NSNumber*>* doomed_uids);
91-
void HandleEvent(NSDictionary<NSString*, id>* annotatedEvent);
9292

9393
FlutterViewController* view_controller_;
9494
PlatformViewIOS* platform_view_;

shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,10 @@ static bool DidFlagChange(const flutter::SemanticsNode& oldNode,
362362
NSString* message = annotatedEvent[@"data"][@"message"];
363363
ios_delegate_->PostAccessibilityNotification(UIAccessibilityAnnouncementNotification, message);
364364
}
365+
if ([type isEqualToString:@"focus"]) {
366+
SemanticsObject* node = objects_.get()[annotatedEvent[@"nodeId"]];
367+
ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, node);
368+
}
365369
}
366370

367371
fml::WeakPtr<AccessibilityBridge> AccessibilityBridge::GetWeakPtr() {

shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,6 +1289,50 @@ - (void)testAnnouncesRouteChangesRemoveRouteInMiddle {
12891289
UIAccessibilityScreenChangedNotification);
12901290
}
12911291

1292+
- (void)testHandleEvent {
1293+
flutter::MockDelegate mock_delegate;
1294+
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
1295+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
1296+
/*platform=*/thread_task_runner,
1297+
/*raster=*/thread_task_runner,
1298+
/*ui=*/thread_task_runner,
1299+
/*io=*/thread_task_runner);
1300+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
1301+
/*delegate=*/mock_delegate,
1302+
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
1303+
/*platform_views_controller=*/nil,
1304+
/*task_runners=*/runners,
1305+
/*worker_task_runner=*/nil,
1306+
/*is_gpu_disabled_sync_switch=*/nil);
1307+
id mockFlutterView = OCMClassMock([FlutterView class]);
1308+
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
1309+
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
1310+
1311+
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
1312+
[[[NSMutableArray alloc] init] autorelease];
1313+
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
1314+
ios_delegate->on_PostAccessibilityNotification_ =
1315+
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
1316+
[accessibility_notifications addObject:@{
1317+
@"notification" : @(notification),
1318+
@"argument" : argument ? argument : [NSNull null],
1319+
}];
1320+
};
1321+
__block auto bridge =
1322+
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
1323+
/*platform_view=*/platform_view.get(),
1324+
/*platform_views_controller=*/nil,
1325+
/*ios_delegate=*/std::move(ios_delegate));
1326+
1327+
NSDictionary<NSString*, id>* annotatedEvent = @{@"type" : @"focus", @"nodeId" : @123};
1328+
1329+
bridge->HandleEvent(annotatedEvent);
1330+
1331+
XCTAssertEqual([accessibility_notifications count], 1ul);
1332+
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
1333+
UIAccessibilityLayoutChangedNotification);
1334+
}
1335+
12921336
- (void)testAnnouncesRouteChangesWhenNoNamesRoute {
12931337
flutter::MockDelegate mock_delegate;
12941338
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");

0 commit comments

Comments
 (0)