• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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#include "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h"
6#include "flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h"
7
8#include <utility>
9#include <vector>
10
11#import <UIKit/UIKit.h>
12
13#include "flutter/fml/logging.h"
14#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
15#include "flutter/shell/platform/darwin/ios/platform_view_ios.h"
16
17namespace {
18
19constexpr int32_t kRootNodeId = 0;
20
21flutter::SemanticsAction GetSemanticsActionForScrollDirection(
22    UIAccessibilityScrollDirection direction) {
23  // To describe scroll direction, UIAccessibilityScrollDirection uses the direction the scroll bar
24  // moves in and SemanticsAction uses the direction the finger moves in. Both move in opposite
25  // directions, which is why the following maps left to right and vice versa.
26  switch (direction) {
27    case UIAccessibilityScrollDirectionRight:
28    case UIAccessibilityScrollDirectionPrevious:  // TODO(abarth): Support RTL using
29                                                  // _node.textDirection.
30      return flutter::SemanticsAction::kScrollLeft;
31    case UIAccessibilityScrollDirectionLeft:
32    case UIAccessibilityScrollDirectionNext:  // TODO(abarth): Support RTL using
33                                              // _node.textDirection.
34      return flutter::SemanticsAction::kScrollRight;
35    case UIAccessibilityScrollDirectionUp:
36      return flutter::SemanticsAction::kScrollDown;
37    case UIAccessibilityScrollDirectionDown:
38      return flutter::SemanticsAction::kScrollUp;
39  }
40  FML_DCHECK(false);  // Unreachable
41  return flutter::SemanticsAction::kScrollUp;
42}
43
44}  // namespace
45
46@implementation FlutterCustomAccessibilityAction {
47}
48@end
49
50/**
51 * Represents a semantics object that has children and hence has to be presented to the OS as a
52 * UIAccessibilityContainer.
53 *
54 * The SemanticsObject class cannot implement the UIAccessibilityContainer protocol because an
55 * object that returns YES for isAccessibilityElement cannot also implement
56 * UIAccessibilityContainer.
57 *
58 * With the help of SemanticsObjectContainer, the hierarchy of semantic objects received from
59 * the framework, such as:
60 *
61 * SemanticsObject1
62 *     SemanticsObject2
63 *         SemanticsObject3
64 *         SemanticsObject4
65 *
66 * is translated into the following hierarchy, which is understood by iOS:
67 *
68 * SemanticsObjectContainer1
69 *     SemanticsObject1
70 *     SemanticsObjectContainer2
71 *         SemanticsObject2
72 *         SemanticsObject3
73 *         SemanticsObject4
74 *
75 * From Flutter's view of the world (the first tree seen above), we construct iOS's view of the
76 * world (second tree) as follows: We replace each SemanticsObjects that has children with a
77 * SemanticsObjectContainer, which has the original SemanticsObject and its children as children.
78 *
79 * SemanticsObjects have semantic information attached to them which is interpreted by
80 * VoiceOver (they return YES for isAccessibilityElement). The SemanticsObjectContainers are just
81 * there for structure and they don't provide any semantic information to VoiceOver (they return
82 * NO for isAccessibilityElement).
83 */
84@interface SemanticsObjectContainer : NSObject
85- (instancetype)init __attribute__((unavailable("Use initWithSemanticsObject instead")));
86- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject
87                                 bridge:(fml::WeakPtr<flutter::AccessibilityBridge>)bridge
88    NS_DESIGNATED_INITIALIZER;
89
90@property(nonatomic, weak) SemanticsObject* semanticsObject;
91
92@end
93
94@implementation SemanticsObject {
95  fml::scoped_nsobject<SemanticsObjectContainer> _container;
96}
97
98#pragma mark - Override base class designated initializers
99
100// Method declared as unavailable in the interface
101- (instancetype)init {
102  [self release];
103  [super doesNotRecognizeSelector:_cmd];
104  return nil;
105}
106
107#pragma mark - Designated initializers
108
109- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridge>)bridge uid:(int32_t)uid {
110  FML_DCHECK(bridge) << "bridge must be set";
111  FML_DCHECK(uid >= kRootNodeId);
112  self = [super init];
113
114  if (self) {
115    _bridge = bridge;
116    _uid = uid;
117    _children = [[NSMutableArray alloc] init];
118  }
119
120  return self;
121}
122
123- (void)dealloc {
124  for (SemanticsObject* child in _children) {
125    child.parent = nil;
126  }
127  [_children removeAllObjects];
128  [_children release];
129  _parent = nil;
130  _container.get().semanticsObject = nil;
131  [_platformViewSemanticsContainer release];
132  [super dealloc];
133}
134
135#pragma mark - Semantic object methods
136
137- (void)setSemanticsNode:(const flutter::SemanticsNode*)node {
138  _node = *node;
139}
140
141/**
142 * Whether calling `setSemanticsNode:` with `node` would cause a layout change.
143 */
144- (BOOL)nodeWillCauseLayoutChange:(const flutter::SemanticsNode*)node {
145  return [self node].rect != node->rect || [self node].transform != node->transform;
146}
147
148/**
149 * Whether calling `setSemanticsNode:` with `node` would cause a scroll event.
150 */
151- (BOOL)nodeWillCauseScroll:(const flutter::SemanticsNode*)node {
152  return !isnan([self node].scrollPosition) && !isnan(node->scrollPosition) &&
153         [self node].scrollPosition != node->scrollPosition;
154}
155
156- (BOOL)hasChildren {
157  if (_node.IsPlatformViewNode()) {
158    return YES;
159  }
160  return [self.children count] != 0;
161}
162
163#pragma mark - UIAccessibility overrides
164
165- (BOOL)isAccessibilityElement {
166  // Note: hit detection will only apply to elements that report
167  // -isAccessibilityElement of YES. The framework will continue scanning the
168  // entire element tree looking for such a hit.
169
170  //  We enforce in the framework that no other useful semantics are merged with these nodes.
171  if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute))
172    return false;
173
174  // If the only flag(s) set are scrolling related AND
175  // The only flags set are not kIsHidden OR
176  // The node doesn't have a label, value, or hint OR
177  // The only actions set are scrolling related actions.
178  //
179  // The kIsHidden flag set with any other flag just means this node is now
180  // hidden but still is a valid target for a11y focus in the tree, e.g. a list
181  // item that is currently off screen but the a11y navigation needs to know
182  // about.
183  return (([self node].flags & ~flutter::kScrollableSemanticsFlags) != 0 &&
184          [self node].flags != static_cast<int32_t>(flutter::SemanticsFlags::kIsHidden)) ||
185         ![self node].label.empty() || ![self node].value.empty() || ![self node].hint.empty() ||
186         ([self node].actions & ~flutter::kScrollableSemanticsActions) != 0;
187}
188
189- (void)collectRoutes:(NSMutableArray<SemanticsObject*>*)edges {
190  if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute))
191    [edges addObject:self];
192  if ([self hasChildren]) {
193    for (SemanticsObject* child in self.children) {
194      [child collectRoutes:edges];
195    }
196  }
197}
198
199- (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action {
200  if (![self node].HasAction(flutter::SemanticsAction::kCustomAction))
201    return NO;
202  int32_t action_id = action.uid;
203  std::vector<uint8_t> args;
204  args.push_back(3);  // type=int32.
205  args.push_back(action_id);
206  args.push_back(action_id >> 8);
207  args.push_back(action_id >> 16);
208  args.push_back(action_id >> 24);
209  [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kCustomAction,
210                                           std::move(args));
211  return YES;
212}
213
214- (NSString*)routeName {
215  // Returns the first non-null and non-empty semantic label of a child
216  // with an NamesRoute flag. Otherwise returns nil.
217  if ([self node].HasFlag(flutter::SemanticsFlags::kNamesRoute)) {
218    NSString* newName = [self accessibilityLabel];
219    if (newName != nil && [newName length] > 0) {
220      return newName;
221    }
222  }
223  if ([self hasChildren]) {
224    for (SemanticsObject* child in self.children) {
225      NSString* newName = [child routeName];
226      if (newName != nil && [newName length] > 0) {
227        return newName;
228      }
229    }
230  }
231  return nil;
232}
233
234- (NSString*)accessibilityLabel {
235  if ([self node].label.empty())
236    return nil;
237  return @([self node].label.data());
238}
239
240- (NSString*)accessibilityHint {
241  if ([self node].hint.empty())
242    return nil;
243  return @([self node].hint.data());
244}
245
246- (NSString*)accessibilityValue {
247  if ([self node].value.empty())
248    return nil;
249  return @([self node].value.data());
250}
251
252- (CGRect)accessibilityFrame {
253  if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) {
254    return [super accessibilityFrame];
255  }
256  return [self globalRect];
257}
258
259- (CGRect)globalRect {
260  SkMatrix44 globalTransform = [self node].transform;
261  for (SemanticsObject* parent = [self parent]; parent; parent = parent.parent) {
262    globalTransform = parent.node.transform * globalTransform;
263  }
264
265  SkPoint quad[4];
266  [self node].rect.toQuad(quad);
267  for (auto& point : quad) {
268    SkScalar vector[4] = {point.x(), point.y(), 0, 1};
269    globalTransform.mapScalars(vector);
270    point.set(vector[0] / vector[3], vector[1] / vector[3]);
271  }
272  SkRect rect;
273  rect.set(quad, 4);
274
275  // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in
276  // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to
277  // convert.
278  CGFloat scale = [[[self bridge]->view() window] screen].scale;
279  auto result =
280      CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale);
281  return UIAccessibilityConvertFrameToScreenCoordinates(result, [self bridge] -> view());
282}
283
284#pragma mark - UIAccessibilityElement protocol
285
286- (id)accessibilityContainer {
287  if ([self hasChildren] || [self uid] == kRootNodeId) {
288    if (_container == nil)
289      _container.reset([[SemanticsObjectContainer alloc] initWithSemanticsObject:self
290                                                                          bridge:[self bridge]]);
291    return _container.get();
292  }
293  if ([self parent] == nil) {
294    // This can happen when we have released the accessibility tree but iOS is
295    // still holding onto our objects. iOS can take some time before it
296    // realizes that the tree has changed.
297    return nil;
298  }
299  return [[self parent] accessibilityContainer];
300}
301
302#pragma mark - UIAccessibilityAction overrides
303
304- (BOOL)accessibilityActivate {
305  if (![self node].HasAction(flutter::SemanticsAction::kTap))
306    return NO;
307  [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kTap);
308  return YES;
309}
310
311- (void)accessibilityIncrement {
312  if ([self node].HasAction(flutter::SemanticsAction::kIncrease)) {
313    [self node].value = [self node].increasedValue;
314    [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kIncrease);
315  }
316}
317
318- (void)accessibilityDecrement {
319  if ([self node].HasAction(flutter::SemanticsAction::kDecrease)) {
320    [self node].value = [self node].decreasedValue;
321    [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDecrease);
322  }
323}
324
325- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
326  flutter::SemanticsAction action = GetSemanticsActionForScrollDirection(direction);
327  if (![self node].HasAction(action))
328    return NO;
329  [self bridge] -> DispatchSemanticsAction([self uid], action);
330  return YES;
331}
332
333- (BOOL)accessibilityPerformEscape {
334  if (![self node].HasAction(flutter::SemanticsAction::kDismiss))
335    return NO;
336  [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDismiss);
337  return YES;
338}
339
340#pragma mark UIAccessibilityFocus overrides
341
342- (void)accessibilityElementDidBecomeFocused {
343  if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) {
344    [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kShowOnScreen);
345  }
346  if ([self node].HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) {
347    [self bridge] -> DispatchSemanticsAction([self uid],
348                                             flutter::SemanticsAction::kDidGainAccessibilityFocus);
349  }
350}
351
352- (void)accessibilityElementDidLoseFocus {
353  if ([self node].HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) {
354    [self bridge] -> DispatchSemanticsAction([self uid],
355                                             flutter::SemanticsAction::kDidLoseAccessibilityFocus);
356  }
357}
358
359@end
360
361@implementation FlutterSemanticsObject {
362}
363
364#pragma mark - Override base class designated initializers
365
366// Method declared as unavailable in the interface
367- (instancetype)init {
368  [self release];
369  [super doesNotRecognizeSelector:_cmd];
370  return nil;
371}
372
373#pragma mark - Designated initializers
374
375- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridge>)bridge uid:(int32_t)uid {
376  self = [super initWithBridge:bridge uid:uid];
377  return self;
378}
379
380#pragma mark - UIAccessibility overrides
381
382- (UIAccessibilityTraits)accessibilityTraits {
383  UIAccessibilityTraits traits = UIAccessibilityTraitNone;
384  if ([self node].HasAction(flutter::SemanticsAction::kIncrease) ||
385      [self node].HasAction(flutter::SemanticsAction::kDecrease)) {
386    traits |= UIAccessibilityTraitAdjustable;
387  }
388  // TODO(jonahwilliams): switches should have a value of "on" or "off"
389  if ([self node].HasFlag(flutter::SemanticsFlags::kIsSelected) ||
390      [self node].HasFlag(flutter::SemanticsFlags::kIsToggled) ||
391      [self node].HasFlag(flutter::SemanticsFlags::kIsChecked)) {
392    traits |= UIAccessibilityTraitSelected;
393  }
394  if ([self node].HasFlag(flutter::SemanticsFlags::kIsButton)) {
395    traits |= UIAccessibilityTraitButton;
396  }
397  if ([self node].HasFlag(flutter::SemanticsFlags::kHasEnabledState) &&
398      ![self node].HasFlag(flutter::SemanticsFlags::kIsEnabled)) {
399    traits |= UIAccessibilityTraitNotEnabled;
400  }
401  if ([self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) {
402    traits |= UIAccessibilityTraitHeader;
403  }
404  if ([self node].HasFlag(flutter::SemanticsFlags::kIsImage)) {
405    traits |= UIAccessibilityTraitImage;
406  }
407  if ([self node].HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
408    traits |= UIAccessibilityTraitUpdatesFrequently;
409  }
410  return traits;
411}
412
413@end
414
415@implementation FlutterPlatformViewSemanticsContainer {
416  SemanticsObject* _semanticsObject;
417  UIView* _platformView;
418}
419
420// Method declared as unavailable in the interface
421- (instancetype)init {
422  [self release];
423  [super doesNotRecognizeSelector:_cmd];
424  return nil;
425}
426
427- (instancetype)initWithSemanticsObject:(SemanticsObject*)object {
428  FML_CHECK(object);
429  if (self = [super init]) {
430    _semanticsObject = object;
431    flutter::FlutterPlatformViewsController* controller =
432        object.bridge->GetPlatformViewsController();
433    if (controller) {
434      _platformView = [controller->GetPlatformViewByID(object.node.platformViewId) view];
435    }
436    self.accessibilityElements = @[ _semanticsObject, _platformView ];
437  }
438  return self;
439}
440
441- (CGRect)accessibilityFrame {
442  return _semanticsObject.accessibilityFrame;
443}
444
445- (BOOL)isAccessibilityElement {
446  return NO;
447}
448
449- (id)accessibilityContainer {
450  return [_semanticsObject accessibilityContainer];
451}
452
453- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
454  return [_platformView accessibilityScroll:direction];
455}
456
457@end
458
459@implementation SemanticsObjectContainer {
460  SemanticsObject* _semanticsObject;
461  fml::WeakPtr<flutter::AccessibilityBridge> _bridge;
462}
463
464#pragma mark - initializers
465
466// Method declared as unavailable in the interface
467- (instancetype)init {
468  [self release];
469  [super doesNotRecognizeSelector:_cmd];
470  return nil;
471}
472
473- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject
474                                 bridge:(fml::WeakPtr<flutter::AccessibilityBridge>)bridge {
475  FML_DCHECK(semanticsObject) << "semanticsObject must be set";
476  self = [super init];
477
478  if (self) {
479    _semanticsObject = semanticsObject;
480    _bridge = bridge;
481  }
482
483  return self;
484}
485
486#pragma mark - UIAccessibilityContainer overrides
487
488- (NSInteger)accessibilityElementCount {
489  NSInteger count = [[_semanticsObject children] count] + 1;
490  return count;
491}
492
493- (nullable id)accessibilityElementAtIndex:(NSInteger)index {
494  if (index < 0 || index >= [self accessibilityElementCount])
495    return nil;
496  if (index == 0) {
497    return _semanticsObject;
498  }
499
500  SemanticsObject* child = [_semanticsObject children][index - 1];
501
502  // Swap the original `SemanticsObject` to a `PlatformViewSemanticsContainer`
503  if (child.node.IsPlatformViewNode()) {
504    child.platformViewSemanticsContainer.index = index;
505    return child.platformViewSemanticsContainer;
506  }
507
508  if ([child hasChildren])
509    return [child accessibilityContainer];
510  return child;
511}
512
513- (NSInteger)indexOfAccessibilityElement:(id)element {
514  if (element == _semanticsObject)
515    return 0;
516
517  // FlutterPlatformViewSemanticsContainer is always the last element of its parent.
518  if ([element isKindOfClass:[FlutterPlatformViewSemanticsContainer class]]) {
519    return ((FlutterPlatformViewSemanticsContainer*)element).index;
520  }
521
522  NSMutableArray<SemanticsObject*>* children = [_semanticsObject children];
523  for (size_t i = 0; i < [children count]; i++) {
524    SemanticsObject* child = children[i];
525    if ((![child hasChildren] && child == element) ||
526        ([child hasChildren] && [child accessibilityContainer] == element))
527      return i + 1;
528  }
529  return NSNotFound;
530}
531
532#pragma mark - UIAccessibilityElement protocol
533
534- (BOOL)isAccessibilityElement {
535  return NO;
536}
537
538- (CGRect)accessibilityFrame {
539  return [_semanticsObject accessibilityFrame];
540}
541
542- (id)accessibilityContainer {
543  if (!_bridge) {
544    return nil;
545  }
546  return ([_semanticsObject uid] == kRootNodeId)
547             ? _bridge->view()
548             : [[_semanticsObject parent] accessibilityContainer];
549}
550
551#pragma mark - UIAccessibilityAction overrides
552
553- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
554  return [_semanticsObject accessibilityScroll:direction];
555}
556
557@end
558
559#pragma mark - AccessibilityBridge impl
560
561namespace flutter {
562
563AccessibilityBridge::AccessibilityBridge(UIView* view,
564                                         PlatformViewIOS* platform_view,
565                                         FlutterPlatformViewsController* platform_views_controller)
566    : view_(view),
567      platform_view_(platform_view),
568      platform_views_controller_(platform_views_controller),
569      objects_([[NSMutableDictionary alloc] init]),
570      weak_factory_(this),
571      previous_route_id_(0),
572      previous_routes_({}) {
573  accessibility_channel_.reset([[FlutterBasicMessageChannel alloc]
574         initWithName:@"flutter/accessibility"
575      binaryMessenger:platform_view->GetOwnerViewController().get().engine.binaryMessenger
576                codec:[FlutterStandardMessageCodec sharedInstance]]);
577  [accessibility_channel_.get() setMessageHandler:^(id message, FlutterReply reply) {
578    HandleEvent((NSDictionary*)message);
579  }];
580}
581
582AccessibilityBridge::~AccessibilityBridge() {
583  clearState();
584  view_.accessibilityElements = nil;
585}
586
587UIView<UITextInput>* AccessibilityBridge::textInputView() {
588  return [platform_view_->GetTextInputPlugin() textInputView];
589}
590
591void AccessibilityBridge::UpdateSemantics(flutter::SemanticsNodeUpdates nodes,
592                                          flutter::CustomAccessibilityActionUpdates actions) {
593  BOOL layoutChanged = NO;
594  BOOL scrollOccured = NO;
595  for (const auto& entry : actions) {
596    const flutter::CustomAccessibilityAction& action = entry.second;
597    actions_[action.id] = action;
598  }
599  for (const auto& entry : nodes) {
600    const flutter::SemanticsNode& node = entry.second;
601    SemanticsObject* object = GetOrCreateObject(node.id, nodes);
602    layoutChanged = layoutChanged || [object nodeWillCauseLayoutChange:&node];
603    scrollOccured = scrollOccured || [object nodeWillCauseScroll:&node];
604    [object setSemanticsNode:&node];
605    NSUInteger newChildCount = node.childrenInTraversalOrder.size();
606    NSMutableArray* newChildren =
607        [[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease];
608    for (NSUInteger i = 0; i < newChildCount; ++i) {
609      SemanticsObject* child = GetOrCreateObject(node.childrenInTraversalOrder[i], nodes);
610      child.parent = object;
611      [newChildren addObject:child];
612    }
613    object.children = newChildren;
614    if (node.customAccessibilityActions.size() > 0) {
615      NSMutableArray<FlutterCustomAccessibilityAction*>* accessibilityCustomActions =
616          [[[NSMutableArray alloc] init] autorelease];
617      for (int32_t action_id : node.customAccessibilityActions) {
618        flutter::CustomAccessibilityAction& action = actions_[action_id];
619        if (action.overrideId != -1) {
620          // iOS does not support overriding standard actions, so we ignore any
621          // custom actions that have an override id provided.
622          continue;
623        }
624        NSString* label = @(action.label.data());
625        SEL selector = @selector(onCustomAccessibilityAction:);
626        FlutterCustomAccessibilityAction* customAction =
627            [[FlutterCustomAccessibilityAction alloc] initWithName:label
628                                                            target:object
629                                                          selector:selector];
630        customAction.uid = action_id;
631        [accessibilityCustomActions addObject:customAction];
632      }
633      object.accessibilityCustomActions = accessibilityCustomActions;
634    }
635
636    if (object.node.IsPlatformViewNode()) {
637      FlutterPlatformViewsController* controller = GetPlatformViewsController();
638      if (controller) {
639        object.platformViewSemanticsContainer =
640            [[FlutterPlatformViewSemanticsContainer alloc] initWithSemanticsObject:object];
641      }
642    } else if (object.platformViewSemanticsContainer) {
643      [object.platformViewSemanticsContainer release];
644    }
645  }
646
647  SemanticsObject* root = objects_.get()[@(kRootNodeId)];
648
649  bool routeChanged = false;
650  SemanticsObject* lastAdded = nil;
651
652  if (root) {
653    if (!view_.accessibilityElements) {
654      view_.accessibilityElements = @[ [root accessibilityContainer] ];
655    }
656    NSMutableArray<SemanticsObject*>* newRoutes = [[[NSMutableArray alloc] init] autorelease];
657    [root collectRoutes:newRoutes];
658    for (SemanticsObject* route in newRoutes) {
659      if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) !=
660          previous_routes_.end()) {
661        lastAdded = route;
662      }
663    }
664    if (lastAdded == nil && [newRoutes count] > 0) {
665      int index = [newRoutes count] - 1;
666      lastAdded = [newRoutes objectAtIndex:index];
667    }
668    if (lastAdded != nil && [lastAdded uid] != previous_route_id_) {
669      previous_route_id_ = [lastAdded uid];
670      routeChanged = true;
671    }
672    previous_routes_.clear();
673    for (SemanticsObject* route in newRoutes) {
674      previous_routes_.push_back([route uid]);
675    }
676  } else {
677    view_.accessibilityElements = nil;
678  }
679
680  NSMutableArray<NSNumber*>* doomed_uids = [NSMutableArray arrayWithArray:[objects_.get() allKeys]];
681  if (root)
682    VisitObjectsRecursivelyAndRemove(root, doomed_uids);
683  [objects_ removeObjectsForKeys:doomed_uids];
684
685  layoutChanged = layoutChanged || [doomed_uids count] > 0;
686  if (routeChanged) {
687    NSString* routeName = [lastAdded routeName];
688    UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, routeName);
689  } else if (layoutChanged) {
690    // TODO(goderbauer): figure out which node to focus next.
691    UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
692  }
693  if (scrollOccured) {
694    // TODO(tvolkert): provide meaningful string (e.g. "page 2 of 5")
695    UIAccessibilityPostNotification(UIAccessibilityPageScrolledNotification, @"");
696  }
697}
698
699void AccessibilityBridge::DispatchSemanticsAction(int32_t uid, flutter::SemanticsAction action) {
700  platform_view_->DispatchSemanticsAction(uid, action, {});
701}
702
703void AccessibilityBridge::DispatchSemanticsAction(int32_t uid,
704                                                  flutter::SemanticsAction action,
705                                                  std::vector<uint8_t> args) {
706  platform_view_->DispatchSemanticsAction(uid, action, std::move(args));
707}
708
709SemanticsObject* AccessibilityBridge::GetOrCreateObject(int32_t uid,
710                                                        flutter::SemanticsNodeUpdates& updates) {
711  SemanticsObject* object = objects_.get()[@(uid)];
712  if (!object) {
713    // New node case: simply create a new SemanticsObject.
714    flutter::SemanticsNode node = updates[uid];
715    if (node.HasFlag(flutter::SemanticsFlags::kIsTextField) &&
716        !node.HasFlag(flutter::SemanticsFlags::kIsReadOnly)) {
717      // Text fields are backed by objects that implement UITextInput.
718      object = [[[TextInputSemanticsObject alloc] initWithBridge:GetWeakPtr() uid:uid] autorelease];
719    } else {
720      object = [[[FlutterSemanticsObject alloc] initWithBridge:GetWeakPtr() uid:uid] autorelease];
721    }
722
723    objects_.get()[@(uid)] = object;
724  } else {
725    // Existing node case
726    auto nodeEntry = updates.find(object.node.id);
727    if (nodeEntry != updates.end()) {
728      // There's an update for this node
729      flutter::SemanticsNode node = nodeEntry->second;
730      BOOL isTextField = node.HasFlag(flutter::SemanticsFlags::kIsTextField);
731      BOOL wasTextField = object.node.HasFlag(flutter::SemanticsFlags::kIsTextField);
732      BOOL isReadOnly = node.HasFlag(flutter::SemanticsFlags::kIsReadOnly);
733      BOOL wasReadOnly = object.node.HasFlag(flutter::SemanticsFlags::kIsReadOnly);
734      if (wasTextField != isTextField || isReadOnly != wasReadOnly) {
735        // The node changed its type from text field to something else, or vice versa. In this
736        // case, we cannot reuse the existing SemanticsObject implementation. Instead, we replace
737        // it with a new instance.
738        NSUInteger positionInChildlist = [object.parent.children indexOfObject:object];
739        SemanticsObject* parent = object.parent;
740        [objects_ removeObjectForKey:@(node.id)];
741        if (isTextField && !isReadOnly) {
742          // Text fields are backed by objects that implement UITextInput.
743          object = [[[TextInputSemanticsObject alloc] initWithBridge:GetWeakPtr()
744                                                                 uid:uid] autorelease];
745        } else {
746          object = [[[FlutterSemanticsObject alloc] initWithBridge:GetWeakPtr()
747                                                               uid:uid] autorelease];
748        }
749        object.parent = parent;
750        [object.parent.children replaceObjectAtIndex:positionInChildlist withObject:object];
751        objects_.get()[@(node.id)] = object;
752      }
753    }
754  }
755  return object;
756}
757
758void AccessibilityBridge::VisitObjectsRecursivelyAndRemove(SemanticsObject* object,
759                                                           NSMutableArray<NSNumber*>* doomed_uids) {
760  [doomed_uids removeObject:@(object.uid)];
761  for (SemanticsObject* child in [object children])
762    VisitObjectsRecursivelyAndRemove(child, doomed_uids);
763}
764
765void AccessibilityBridge::HandleEvent(NSDictionary<NSString*, id>* annotatedEvent) {
766  NSString* type = annotatedEvent[@"type"];
767  if ([type isEqualToString:@"announce"]) {
768    NSString* message = annotatedEvent[@"data"][@"message"];
769    UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, message);
770  }
771}
772
773fml::WeakPtr<AccessibilityBridge> AccessibilityBridge::GetWeakPtr() {
774  return weak_factory_.GetWeakPtr();
775}
776
777void AccessibilityBridge::clearState() {
778  [objects_ removeAllObjects];
779  previous_route_id_ = 0;
780  previous_routes_.clear();
781}
782
783}  // namespace flutter
784