Flutter Engine
The Flutter Engine
accessibility_bridge.mm
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h"
6
7#include <utility>
8
9#include "flutter/fml/logging.h"
10#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h"
11#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
12#import "flutter/shell/platform/darwin/ios/framework/Source/TextInputSemanticsObject.h"
13#import "flutter/shell/platform/darwin/ios/platform_view_ios.h"
14
15#pragma GCC diagnostic error "-Wundeclared-selector"
16
18
19namespace flutter {
20namespace {
21
22constexpr int32_t kSemanticObjectIdInvalid = -1;
23
24class DefaultIosDelegate : public AccessibilityBridge::IosDelegate {
25 public:
26 bool IsFlutterViewControllerPresentingModalViewController(
27 FlutterViewController* view_controller) override {
28 if (view_controller) {
29 return view_controller.isPresentingViewController;
30 } else {
31 return false;
32 }
33 }
34
35 void PostAccessibilityNotification(UIAccessibilityNotifications notification,
36 id argument) override {
37 UIAccessibilityPostNotification(notification, argument);
38 }
39};
40} // namespace
41
43 FlutterViewController* view_controller,
44 PlatformViewIOS* platform_view,
45 std::shared_ptr<FlutterPlatformViewsController> platform_views_controller,
46 std::unique_ptr<IosDelegate> ios_delegate)
47 : view_controller_(view_controller),
48 platform_view_(platform_view),
49 platform_views_controller_(std::move(platform_views_controller)),
50 last_focused_semantics_object_id_(kSemanticObjectIdInvalid),
51 objects_([[NSMutableDictionary alloc] init]),
52 previous_routes_({}),
53 ios_delegate_(ios_delegate ? std::move(ios_delegate)
54 : std::make_unique<DefaultIosDelegate>()),
55 weak_factory_(this) {
56 accessibility_channel_.reset([[FlutterBasicMessageChannel alloc]
57 initWithName:@"flutter/accessibility"
58 binaryMessenger:platform_view->GetOwnerViewController().get().engine.binaryMessenger
59 codec:[FlutterStandardMessageCodec sharedInstance]]);
60 [accessibility_channel_.get() setMessageHandler:^(id message, FlutterReply reply) {
61 HandleEvent((NSDictionary*)message);
62 }];
63}
64
65AccessibilityBridge::~AccessibilityBridge() {
66 [accessibility_channel_.get() setMessageHandler:nil];
67 clearState();
68 view_controller_.viewIfLoaded.accessibilityElements = nil;
69}
70
71UIView<UITextInput>* AccessibilityBridge::textInputView() {
72 return [[platform_view_->GetOwnerViewController().get().engine textInputPlugin] textInputView];
73}
74
75void AccessibilityBridge::AccessibilityObjectDidBecomeFocused(int32_t id) {
76 last_focused_semantics_object_id_ = id;
77 [accessibility_channel_.get() sendMessage:@{@"type" : @"didGainFocus", @"nodeId" : @(id)}];
78}
79
80void AccessibilityBridge::AccessibilityObjectDidLoseFocus(int32_t id) {
81 if (last_focused_semantics_object_id_ == id) {
82 last_focused_semantics_object_id_ = kSemanticObjectIdInvalid;
83 }
84}
85
86void AccessibilityBridge::UpdateSemantics(
89 BOOL layoutChanged = NO;
90 BOOL scrollOccured = NO;
91 BOOL needsAnnouncement = NO;
92 for (const auto& entry : actions) {
93 const flutter::CustomAccessibilityAction& action = entry.second;
94 actions_[action.id] = action;
95 }
96 for (const auto& entry : nodes) {
97 const flutter::SemanticsNode& node = entry.second;
98 SemanticsObject* object = GetOrCreateObject(node.id, nodes);
99 layoutChanged = layoutChanged || [object nodeWillCauseLayoutChange:&node];
100 scrollOccured = scrollOccured || [object nodeWillCauseScroll:&node];
101 needsAnnouncement = [object nodeShouldTriggerAnnouncement:&node];
102 [object setSemanticsNode:&node];
103 NSUInteger newChildCount = node.childrenInTraversalOrder.size();
104 NSMutableArray* newChildren =
105 [[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease];
106 for (NSUInteger i = 0; i < newChildCount; ++i) {
107 SemanticsObject* child = GetOrCreateObject(node.childrenInTraversalOrder[i], nodes);
108 [newChildren addObject:child];
109 }
110 NSMutableArray* newChildrenInHitTestOrder =
111 [[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease];
112 for (NSUInteger i = 0; i < newChildCount; ++i) {
113 SemanticsObject* child = GetOrCreateObject(node.childrenInHitTestOrder[i], nodes);
114 [newChildrenInHitTestOrder addObject:child];
115 }
116 object.children = newChildren;
117 object.childrenInHitTestOrder = newChildrenInHitTestOrder;
118 if (!node.customAccessibilityActions.empty()) {
119 NSMutableArray<FlutterCustomAccessibilityAction*>* accessibilityCustomActions =
120 [[[NSMutableArray alloc] init] autorelease];
121 for (int32_t action_id : node.customAccessibilityActions) {
122 flutter::CustomAccessibilityAction& action = actions_[action_id];
123 if (action.overrideId != -1) {
124 // iOS does not support overriding standard actions, so we ignore any
125 // custom actions that have an override id provided.
126 continue;
127 }
128 NSString* label = @(action.label.data());
129 SEL selector = @selector(onCustomAccessibilityAction:);
131 [[[FlutterCustomAccessibilityAction alloc] initWithName:label
132 target:object
133 selector:selector] autorelease];
134 customAction.uid = action_id;
135 [accessibilityCustomActions addObject:customAction];
136 }
137 object.accessibilityCustomActions = accessibilityCustomActions;
138 }
139
140 if (needsAnnouncement) {
141 // Try to be more polite - iOS 11+ supports
142 // UIAccessibilitySpeechAttributeQueueAnnouncement which should avoid
143 // interrupting system notifications or other elements.
144 // Expectation: roughly match the behavior of polite announcements on
145 // Android.
146 NSString* announcement =
147 [[[NSString alloc] initWithUTF8String:object.node.label.c_str()] autorelease];
148 UIAccessibilityPostNotification(
149 UIAccessibilityAnnouncementNotification,
150 [[[NSAttributedString alloc] initWithString:announcement
151 attributes:@{
152 UIAccessibilitySpeechAttributeQueueAnnouncement : @YES
153 }] autorelease]);
154 }
155 }
156
157 SemanticsObject* root = objects_.get()[@(kRootNodeId)];
158
159 bool routeChanged = false;
160 SemanticsObject* lastAdded = nil;
161
162 if (root) {
163 if (!view_controller_.view.accessibilityElements) {
164 view_controller_.view.accessibilityElements =
165 @[ [root accessibilityContainer] ?: [NSNull null] ];
166 }
167 NSMutableArray<SemanticsObject*>* newRoutes = [[[NSMutableArray alloc] init] autorelease];
168 [root collectRoutes:newRoutes];
169 // Finds the last route that is not in the previous routes.
170 for (SemanticsObject* route in newRoutes) {
171 if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) ==
172 previous_routes_.end()) {
173 lastAdded = route;
174 }
175 }
176 // If all the routes are in the previous route, get the last route.
177 if (lastAdded == nil && [newRoutes count] > 0) {
178 int index = [newRoutes count] - 1;
179 lastAdded = [newRoutes objectAtIndex:index];
180 }
181 // There are two cases if lastAdded != nil
182 // 1. lastAdded is not in previous routes. In this case,
183 // [lastAdded uid] != previous_route_id_
184 // 2. All new routes are in previous routes and
185 // lastAdded = newRoutes.last.
186 // In the first case, we need to announce new route. In the second case,
187 // we need to announce if one list is shorter than the other.
188 if (lastAdded != nil &&
189 ([lastAdded uid] != previous_route_id_ || [newRoutes count] != previous_routes_.size())) {
190 previous_route_id_ = [lastAdded uid];
191 routeChanged = true;
192 }
193 previous_routes_.clear();
194 for (SemanticsObject* route in newRoutes) {
195 previous_routes_.push_back([route uid]);
196 }
197 } else {
198 view_controller_.viewIfLoaded.accessibilityElements = nil;
199 }
200
201 NSMutableArray<NSNumber*>* doomed_uids = [NSMutableArray arrayWithArray:[objects_ allKeys]];
202 if (root) {
203 VisitObjectsRecursivelyAndRemove(root, doomed_uids);
204 }
205 [objects_ removeObjectsForKeys:doomed_uids];
206
207 for (SemanticsObject* object in [objects_ allValues]) {
208 [object accessibilityBridgeDidFinishUpdate];
209 }
210
211 if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_controller_)) {
212 layoutChanged = layoutChanged || [doomed_uids count] > 0;
213
214 if (routeChanged) {
215 NSString* routeName = [lastAdded routeName];
216 ios_delegate_->PostAccessibilityNotification(UIAccessibilityScreenChangedNotification,
217 routeName);
218 }
219
220 if (layoutChanged) {
221 SemanticsObject* next = FindNextFocusableIfNecessary();
222 SemanticsObject* lastFocused =
223 [objects_.get() objectForKey:@(last_focused_semantics_object_id_)];
224 // Only specify the focus item if the new focus is different, avoiding double focuses on the
225 // same item. See: https://github.com/flutter/flutter/issues/104176. If there is a route
226 // change, we always refocus.
227 ios_delegate_->PostAccessibilityNotification(
228 UIAccessibilityLayoutChangedNotification,
229 (routeChanged || next != lastFocused) ? next.nativeAccessibility : NULL);
230 } else if (scrollOccured) {
231 // TODO(chunhtai): figure out what string to use for notification. At this
232 // point, it is guarantee the previous focused object is still in the tree
233 // so that we don't need to worry about focus lost. (e.g. "Screen 0 of 3")
234 ios_delegate_->PostAccessibilityNotification(
235 UIAccessibilityPageScrolledNotification,
236 FindNextFocusableIfNecessary().nativeAccessibility);
237 }
238 }
239}
240
242 platform_view_->DispatchSemanticsAction(uid, action, {});
243}
244
248 platform_view_->DispatchSemanticsAction(uid, action, std::move(args));
249}
250
251static void ReplaceSemanticsObject(SemanticsObject* oldObject,
252 SemanticsObject* newObject,
253 NSMutableDictionary<NSNumber*, SemanticsObject*>* objects) {
254 // `newObject` should represent the same id as `oldObject`.
255 FML_DCHECK(oldObject.node.id == newObject.uid);
256 NSNumber* nodeId = @(oldObject.node.id);
257 NSUInteger positionInChildlist = [oldObject.parent.children indexOfObject:oldObject];
258 [[oldObject retain] autorelease];
259 oldObject.children = @[];
260 [oldObject.parent replaceChildAtIndex:positionInChildlist withChild:newObject];
261 [objects removeObjectForKey:nodeId];
262 objects[nodeId] = newObject;
263}
264
265static SemanticsObject* CreateObject(const flutter::SemanticsNode& node,
266 const fml::WeakPtr<AccessibilityBridge>& weak_ptr) {
269 // Text fields are backed by objects that implement UITextInput.
270 return [[[TextInputSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id] autorelease];
274 return [[[FlutterSwitchSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id] autorelease];
276 return [[[FlutterScrollableSemanticsObject alloc] initWithBridge:weak_ptr
277 uid:node.id] autorelease];
278 } else if (node.IsPlatformViewNode()) {
280 initWithBridge:weak_ptr
281 uid:node.id
282 platformView:weak_ptr->GetPlatformViewsController()->GetFlutterTouchInterceptingViewByID(
283 node.platformViewId)] autorelease];
284 } else {
285 return [[[FlutterSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id] autorelease];
286 }
287}
288
289static bool DidFlagChange(const flutter::SemanticsNode& oldNode,
290 const flutter::SemanticsNode& newNode,
292 return oldNode.HasFlag(flag) != newNode.HasFlag(flag);
293}
294
295SemanticsObject* AccessibilityBridge::GetOrCreateObject(int32_t uid,
297 SemanticsObject* object = objects_.get()[@(uid)];
298 if (!object) {
299 object = CreateObject(updates[uid], GetWeakPtr());
300 objects_.get()[@(uid)] = object;
301 } else {
302 // Existing node case
303 auto nodeEntry = updates.find(object.node.id);
304 if (nodeEntry != updates.end()) {
305 // There's an update for this node
306 flutter::SemanticsNode node = nodeEntry->second;
307 if (DidFlagChange(object.node, node, flutter::SemanticsFlags::kIsTextField) ||
308 DidFlagChange(object.node, node, flutter::SemanticsFlags::kIsReadOnly) ||
309 DidFlagChange(object.node, node, flutter::SemanticsFlags::kHasCheckedState) ||
310 DidFlagChange(object.node, node, flutter::SemanticsFlags::kHasToggledState) ||
311 DidFlagChange(object.node, node, flutter::SemanticsFlags::kHasImplicitScrolling)) {
312 // The node changed its type. In this case, we cannot reuse the existing
313 // SemanticsObject implementation. Instead, we replace it with a new
314 // instance.
315 SemanticsObject* newSemanticsObject = CreateObject(node, GetWeakPtr());
316 ReplaceSemanticsObject(object, newSemanticsObject, objects_.get());
317 object = newSemanticsObject;
318 }
319 }
320 }
321 return object;
322}
323
324void AccessibilityBridge::VisitObjectsRecursivelyAndRemove(SemanticsObject* object,
325 NSMutableArray<NSNumber*>* doomed_uids) {
326 [doomed_uids removeObject:@(object.uid)];
327 for (SemanticsObject* child in [object children])
328 VisitObjectsRecursivelyAndRemove(child, doomed_uids);
329}
330
331SemanticsObject* AccessibilityBridge::FindNextFocusableIfNecessary() {
332 // This property will be -1 if the focus is outside of the flutter
333 // application. In this case, we should not refocus anything.
334 if (last_focused_semantics_object_id_ == kSemanticObjectIdInvalid) {
335 return nil;
336 }
337
338 // Tries to refocus the previous focused semantics object to avoid random jumps.
339 return FindFirstFocusable([objects_.get() objectForKey:@(last_focused_semantics_object_id_)]);
340}
341
342SemanticsObject* AccessibilityBridge::FindFirstFocusable(SemanticsObject* parent) {
343 SemanticsObject* currentObject = parent ?: objects_.get()[@(kRootNodeId)];
344 if (!currentObject) {
345 return nil;
346 }
347 if (currentObject.isAccessibilityElement) {
348 return currentObject;
349 }
350
351 for (SemanticsObject* child in [currentObject children]) {
352 SemanticsObject* candidate = FindFirstFocusable(child);
353 if (candidate) {
354 return candidate;
355 }
356 }
357 return nil;
358}
359
360void AccessibilityBridge::HandleEvent(NSDictionary<NSString*, id>* annotatedEvent) {
361 NSString* type = annotatedEvent[@"type"];
362 if ([type isEqualToString:@"announce"]) {
363 NSString* message = annotatedEvent[@"data"][@"message"];
364 ios_delegate_->PostAccessibilityNotification(UIAccessibilityAnnouncementNotification, message);
365 }
366 if ([type isEqualToString:@"focus"]) {
367 SemanticsObject* node = objects_.get()[annotatedEvent[@"nodeId"]];
368 ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, node);
369 }
370}
371
372fml::WeakPtr<AccessibilityBridge> AccessibilityBridge::GetWeakPtr() {
373 return weak_factory_.GetWeakPtr();
374}
375
376void AccessibilityBridge::clearState() {
377 [objects_ removeAllObjects];
378 previous_route_id_ = 0;
379 previous_routes_.clear();
380}
381
382} // namespace flutter
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterReply)(id _Nullable reply)
std::unique_ptr< flutter::PlatformViewIOS > platform_view
#define FLUTTER_ASSERT_NOT_ARC
Definition: FlutterMacros.h:45
int count
Definition: FontMgrTest.cpp:50
static float next(float f)
constexpr int32_t kRootNodeId
int find(T *array, int N, T item)
GLenum type
AccessibilityBridge()
Creates a new instance of a accessibility bridge.
A Mapping like NonOwnedMapping, but uses Free as its release proc.
Definition: mapping.h:144
FlutterSemanticsFlag flag
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
uint32_t * target
#define FML_DCHECK(condition)
Definition: logging.h:103
SemanticsObject * parent
flutter::SemanticsNode node
NSArray< SemanticsObject * > * children
FlutterTextInputPlugin * textInputPlugin
Win32Message message
static bool init()
std::unordered_map< int32_t, SemanticsNode > SemanticsNodeUpdates
std::unordered_map< int32_t, CustomAccessibilityAction > CustomAccessibilityActionUpdates
static void DispatchSemanticsAction(JNIEnv *env, jobject jcaller, jlong shell_holder, jint id, jint action, jobject args, jint args_position)
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir Path to the cache directory This is different from the persistent_cache_path in embedder which is used for Skia shader cache icu native lib Path to the library file that exports the ICU data vm service The hostname IP address on which the Dart VM Service should be served If not defaults to or::depending on whether ipv6 is specified vm service A custom Dart VM Service port The default is to pick a randomly available open port disable vm Disable the Dart VM Service The Dart VM Service is never available in release mode disable vm service Disable mDNS Dart VM Service publication Bind to the IPv6 localhost address for the Dart VM Service Ignored if vm service host is set endless trace Enable an endless trace buffer The default is a ring buffer This is useful when very old events need to viewed For during application launch Memory usage will continue to grow indefinitely however route
Definition: switches.h:137
string root
Definition: scale_cpu.py:20
Definition: ref_ptr.h:256
std::vector< int32_t > childrenInHitTestOrder
bool HasFlag(SemanticsFlags flag) const
std::vector< int32_t > customAccessibilityActions
bool IsPlatformViewNode() const
std::vector< int32_t > childrenInTraversalOrder
const uintptr_t id
int BOOL
Definition: windows_types.h:37