Flutter Engine
 
Loading...
Searching...
No Matches
accessibility_bridge.cc
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
6
7#include <lib/inspect/cpp/inspector.h>
8#include <lib/zx/process.h>
9#include <zircon/status.h>
10#include <zircon/types.h>
11
12#include <algorithm>
13#include <deque>
14
15#include "flutter/fml/logging.h"
17
18#include "../runtime/dart/utils/root_inspect_node.h"
19
20namespace flutter_runner {
21namespace {
22
23#if !FLUTTER_RELEASE
24static constexpr char kTreeDumpInspectRootName[] = "semantic_tree_root";
25
26// Converts flutter semantic node flags to a string representation.
27std::string NodeFlagsToString(const flutter::SemanticsNode& node) {
28 std::string output;
30 output += "kHasCheckedState|";
31 }
33 output += "kHasEnabledState|";
34 }
35 if (node.flags.hasImplicitScrolling) {
36 output += "kHasImplicitScrolling|";
37 }
39 output += "kHasToggledState|";
40 }
41 if (node.flags.isButton) {
42 output += "kIsButton|";
43 }
45 output += "kIsChecked|";
46 }
48 output += "kIsEnabled|";
49 }
51 output += "kIsFocusable|";
52 }
54 output += "kIsFocused|";
55 }
56 if (node.flags.isHeader) {
57 output += "kIsHeader|";
58 }
59 if (node.flags.isHidden) {
60 output += "kIsHidden|";
61 }
62 if (node.flags.isImage) {
63 output += "kIsImage|";
64 }
66 output += "kIsInMutuallyExclusiveGroup|";
67 }
68 if (node.flags.isKeyboardKey) {
69 output += "kIsKeyboardKey|";
70 }
71 if (node.flags.isLink) {
72 output += "kIsLink|";
73 }
74 if (node.flags.isLiveRegion) {
75 output += "kIsLiveRegion|";
76 }
77 if (node.flags.isObscured) {
78 output += "kIsObscured|";
79 }
80 if (node.flags.isReadOnly) {
81 output += "kIsReadOnly|";
82 }
84 output += "kIsSelected|";
85 }
86 if (node.flags.isSlider) {
87 output += "kIsSlider|";
88 }
89 if (node.flags.isTextField) {
90 output += "kIsTextField|";
91 }
93 output += "kIsToggled|";
94 }
95 if (node.flags.namesRoute) {
96 output += "kNamesRoute|";
97 }
98 if (node.flags.scopesRoute) {
99 output += "kScopesRoute|";
100 }
101
102 return output;
103}
104
105// Converts flutter semantic node actions to a string representation.
106std::string NodeActionsToString(const flutter::SemanticsNode& node) {
107 std::string output;
108
110 output += "kCopy|";
111 }
113 output += "kCustomAction|";
114 }
116 output += "kCut|";
117 }
119 output += "kDecrease|";
120 }
122 output += "kDidGainAccessibilityFocus|";
123 }
125 output += "kDidLoseAccessibilityFocus|";
126 }
128 output += "kDismiss|";
129 }
131 output += "kIncrease|";
132 }
134 output += "kLongPress|";
135 }
136 if (node.HasAction(
138 output += "kMoveCursorBackwardByCharacter|";
139 }
141 output += "kMoveCursorBackwardByWord|";
142 }
144 output += "kMoveCursorForwardByCharacter|";
145 }
147 output += "kMoveCursorForwardByWord|";
148 }
150 output += "kPaste|";
151 }
153 output += "kScrollDown|";
154 }
156 output += "kScrollLeft|";
157 }
159 output += "kScrollRight|";
160 }
162 output += "kScrollUp|";
163 }
165 output += "kScrollToOffset|";
166 }
168 output += "kSetSelection|";
169 }
171 output += "kSetText|";
172 }
174 output += "kShowOnScreen|";
175 }
177 output += "kTap|";
178 }
180 output += "kFocus|";
181 }
183 output += "kExpand|";
184 }
186 output += "kCollapse|";
187 }
188
189 return output;
190}
191
192// Returns a string representation of the flutter semantic node absolut
193// location.
194std::string NodeLocationToString(const SkRect& rect) {
195 auto min_x = rect.fLeft;
196 auto min_y = rect.fTop;
197 auto max_x = rect.fRight;
198 auto max_y = rect.fBottom;
199 std::string location =
200 "min(" + std::to_string(min_x) + ", " + std::to_string(min_y) + ") max(" +
201 std::to_string(max_x) + ", " + std::to_string(max_y) + ")";
202
203 return location;
204}
205
206// Returns a string representation of the node's different types of children.
207std::string NodeChildrenToString(const flutter::SemanticsNode& node) {
208 std::stringstream output;
209 if (!node.childrenInTraversalOrder.empty()) {
210 output << "children in traversal order:[";
211 for (const auto child_id : node.childrenInTraversalOrder) {
212 output << child_id << ", ";
213 }
214 output << "]\n";
215 }
216 if (!node.childrenInHitTestOrder.empty()) {
217 output << "children in hit test order:[";
218 for (const auto child_id : node.childrenInHitTestOrder) {
219 output << child_id << ", ";
220 }
221 output << ']';
222 }
223
224 return output.str();
225}
226#endif // !FLUTTER_RELEASE
227
228} // namespace
229
231 SetSemanticsEnabledCallback set_semantics_enabled_callback,
232 DispatchSemanticsActionCallback dispatch_semantics_action_callback,
233 fuchsia::accessibility::semantics::SemanticsManagerHandle semantics_manager,
234 fuchsia::ui::views::ViewRef view_ref,
235 inspect::Node inspect_node)
236 : set_semantics_enabled_callback_(
237 std::move(set_semantics_enabled_callback)),
238 dispatch_semantics_action_callback_(
239 std::move(dispatch_semantics_action_callback)),
240 binding_(this),
241 fuchsia_semantics_manager_(semantics_manager.Bind()),
242 atomic_updates_(std::make_shared<std::queue<FuchsiaAtomicUpdate>>()),
243 inspect_node_(std::move(inspect_node)) {
244 fuchsia_semantics_manager_.set_error_handler([](zx_status_t status) {
245 FML_LOG(ERROR) << "Flutter cannot connect to SemanticsManager with status: "
246 << zx_status_get_string(status) << ".";
247 });
248 fuchsia_semantics_manager_->RegisterViewForSemantics(
249 std::move(view_ref), binding_.NewBinding(), tree_ptr_.NewRequest());
250
251#if !FLUTTER_RELEASE
252 // The first argument to |CreateLazyValues| is the name of the lazy node, and
253 // will only be displayed if the callback used to generate the node's content
254 // fails. Therefore, we use an error message for this node name.
255 inspect_node_tree_dump_ =
256 inspect_node_.CreateLazyValues("dump_fail", [this]() {
257 inspect::Inspector inspector;
258 if (auto it = nodes_.find(kRootNodeId); it == nodes_.end()) {
259 inspector.GetRoot().CreateString(
260 "empty_tree", "this semantic tree is empty", &inspector);
261 } else {
262 FillInspectTree(
263 kRootNodeId, /*current_level=*/1,
264 inspector.GetRoot().CreateChild(kTreeDumpInspectRootName),
265 &inspector);
266 }
267 return fpromise::make_ok_promise(std::move(inspector));
268 });
269#endif // !FLUTTER_RELEASE
270}
271
273 return semantics_enabled_;
274}
275
277 semantics_enabled_ = enabled;
278 if (!enabled) {
279 nodes_.clear();
280 }
281}
282
283fuchsia::ui::gfx::BoundingBox AccessibilityBridge::GetNodeLocation(
284 const flutter::SemanticsNode& node) const {
285 fuchsia::ui::gfx::BoundingBox box;
286 box.min.x = node.rect.fLeft;
287 box.min.y = node.rect.fTop;
288 box.max.x = node.rect.fRight;
289 box.max.y = node.rect.fBottom;
290 return box;
291}
292
293fuchsia::ui::gfx::mat4 AccessibilityBridge::GetNodeTransform(
294 const flutter::SemanticsNode& node) const {
295 return ConvertSkiaTransformToMat4(node.transform);
296}
297
298fuchsia::ui::gfx::mat4 AccessibilityBridge::ConvertSkiaTransformToMat4(
299 const SkM44 transform) const {
300 fuchsia::ui::gfx::mat4 value;
301 float* m = value.matrix.data();
302 transform.getColMajor(m);
303 return value;
304}
305
306fuchsia::accessibility::semantics::Attributes
307AccessibilityBridge::GetNodeAttributes(const flutter::SemanticsNode& node,
308 size_t* added_size) const {
309 fuchsia::accessibility::semantics::Attributes attributes;
310 // TODO(MI4-2531): Don't truncate.
311 if (node.label.size() > fuchsia::accessibility::semantics::MAX_LABEL_SIZE) {
312 attributes.set_label(node.label.substr(
313 0, fuchsia::accessibility::semantics::MAX_LABEL_SIZE));
314 *added_size += fuchsia::accessibility::semantics::MAX_LABEL_SIZE;
315 } else {
316 attributes.set_label(node.label);
317 *added_size += node.label.size();
318 }
319
320 if (node.tooltip.size() > fuchsia::accessibility::semantics::MAX_LABEL_SIZE) {
321 attributes.set_secondary_label(node.tooltip.substr(
322 0, fuchsia::accessibility::semantics::MAX_LABEL_SIZE));
323 *added_size += fuchsia::accessibility::semantics::MAX_LABEL_SIZE;
324 } else {
325 attributes.set_secondary_label(node.tooltip);
326 *added_size += node.tooltip.size();
327 }
328
329 if (node.flags.isKeyboardKey) {
330 attributes.set_is_keyboard_key(true);
331 }
332
333 return attributes;
334}
335
336fuchsia::accessibility::semantics::States AccessibilityBridge::GetNodeStates(
337 const flutter::SemanticsNode& node,
338 size_t* additional_size) const {
339 fuchsia::accessibility::semantics::States states;
340 (*additional_size) += sizeof(fuchsia::accessibility::semantics::States);
341
342 // Set checked state.
344 states.set_checked_state(
345 fuchsia::accessibility::semantics::CheckedState::NONE);
346 } else {
347 states.set_checked_state(
349 ? fuchsia::accessibility::semantics::CheckedState::CHECKED
350 : fuchsia::accessibility::semantics::CheckedState::UNCHECKED);
351 }
352
353 // Set enabled state.
355 states.set_enabled_state(
357 ? fuchsia::accessibility::semantics::EnabledState::ENABLED
358 : fuchsia::accessibility::semantics::EnabledState::DISABLED);
359 }
360
361 // Set selected state.
362 states.set_selected(node.flags.isSelected ==
364
365 // Flutter's definition of a hidden node is different from Fuchsia, so it must
366 // not be set here.
367
368 // Set value.
369 if (node.value.size() > fuchsia::accessibility::semantics::MAX_VALUE_SIZE) {
370 states.set_value(node.value.substr(
371 0, fuchsia::accessibility::semantics::MAX_VALUE_SIZE));
372 (*additional_size) += fuchsia::accessibility::semantics::MAX_VALUE_SIZE;
373 } else {
374 states.set_value(node.value);
375 (*additional_size) += node.value.size();
376 }
377
378 // Set toggled state.
380 states.set_toggled_state(
382 ? fuchsia::accessibility::semantics::ToggledState::ON
383 : fuchsia::accessibility::semantics::ToggledState::OFF);
384 }
385
386 return states;
387}
388
389std::vector<fuchsia::accessibility::semantics::Action>
390AccessibilityBridge::GetNodeActions(const flutter::SemanticsNode& node,
391 size_t* additional_size) const {
392 std::vector<fuchsia::accessibility::semantics::Action> node_actions;
393
395 node_actions.push_back(fuchsia::accessibility::semantics::Action::DEFAULT);
396 }
398 node_actions.push_back(
399 fuchsia::accessibility::semantics::Action::SECONDARY);
400 }
402 node_actions.push_back(
403 fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN);
404 }
406 node_actions.push_back(
407 fuchsia::accessibility::semantics::Action::INCREMENT);
408 }
410 node_actions.push_back(
411 fuchsia::accessibility::semantics::Action::DECREMENT);
412 }
413
414 *additional_size +=
415 node_actions.size() * sizeof(fuchsia::accessibility::semantics::Action);
416 return node_actions;
417}
418
419fuchsia::accessibility::semantics::Role AccessibilityBridge::GetNodeRole(
420 const flutter::SemanticsNode& node) const {
421 if (node.flags.isButton) {
422 return fuchsia::accessibility::semantics::Role::BUTTON;
423 }
424
425 if (node.flags.isTextField) {
426 return fuchsia::accessibility::semantics::Role::TEXT_FIELD;
427 }
428
429 if (node.flags.isLink) {
430 return fuchsia::accessibility::semantics::Role::LINK;
431 }
432
433 if (node.flags.isSlider) {
434 return fuchsia::accessibility::semantics::Role::SLIDER;
435 }
436
437 if (node.flags.isHeader) {
438 return fuchsia::accessibility::semantics::Role::HEADER;
439 }
440 if (node.flags.isImage) {
441 return fuchsia::accessibility::semantics::Role::IMAGE;
442 }
443
444 // If a flutter node supports the kIncrease or kDecrease actions, it can be
445 // treated as a slider control by assistive technology. This is important
446 // because users have special gestures to deal with sliders, and Fuchsia API
447 // requires nodes that can receive this kind of action to be a slider control.
450 return fuchsia::accessibility::semantics::Role::SLIDER;
451 }
452
453 // If a flutter node has a checked state, then we assume it is either a
454 // checkbox or a radio button. We distinguish between checkboxes and
455 // radio buttons based on membership in a mutually exclusive group.
458 return fuchsia::accessibility::semantics::Role::RADIO_BUTTON;
459 } else {
460 return fuchsia::accessibility::semantics::Role::CHECK_BOX;
461 }
462 }
463
465 return fuchsia::accessibility::semantics::Role::TOGGLE_SWITCH;
466 }
467 return fuchsia::accessibility::semantics::Role::UNKNOWN;
468}
469
470std::unordered_set<int32_t> AccessibilityBridge::GetDescendants(
471 int32_t node_id) const {
472 std::unordered_set<int32_t> descendents;
473 std::deque<int32_t> to_process = {node_id};
474 while (!to_process.empty()) {
475 int32_t id = to_process.front();
476 to_process.pop_front();
477 descendents.emplace(id);
478
479 auto it = nodes_.find(id);
480 if (it != nodes_.end()) {
481 const auto& node = it->second.data;
482 for (const auto& child : node.childrenInHitTestOrder) {
483 if (descendents.find(child) == descendents.end()) {
484 to_process.push_back(child);
485 } else {
486 // This indicates either a cycle or a child with multiple parents.
487 // Flutter should never let this happen, but the engine API does not
488 // explicitly forbid it right now.
489 // TODO(http://fxbug.dev/75905): Crash flutter accessibility bridge
490 // when a cycle in the tree is found.
491 FML_LOG(ERROR) << "Semantics Node " << child
492 << " has already been listed as a child of another "
493 "node, ignoring for parent "
494 << id << ".";
495 }
496 }
497 }
498 }
499 return descendents;
500}
501
502// The only known usage of a negative number for a node ID is in the embedder
503// API as a sentinel value, which is not expected here. No valid producer of
504// nodes should give us a negative ID.
505static uint32_t FlutterIdToFuchsiaId(int32_t flutter_node_id) {
506 FML_DCHECK(flutter_node_id >= 0)
507 << "Unexpectedly received a negative semantics node ID.";
508 return static_cast<uint32_t>(flutter_node_id);
509}
510
511void AccessibilityBridge::PruneUnreachableNodes(
512 FuchsiaAtomicUpdate* atomic_update) {
513 const auto& reachable_nodes = GetDescendants(kRootNodeId);
514 auto iter = nodes_.begin();
515 while (iter != nodes_.end()) {
516 int32_t id = iter->first;
517 if (reachable_nodes.find(id) == reachable_nodes.end()) {
518 atomic_update->AddNodeDeletion(FlutterIdToFuchsiaId(id));
519 iter = nodes_.erase(iter);
520 } else {
521 iter++;
522 }
523 }
524}
525
526// TODO(FIDL-718) - remove this, handle the error instead in something like
527// set_error_handler.
528static void PrintNodeSizeError(uint32_t node_id) {
529 FML_LOG(ERROR) << "Semantics node with ID " << node_id
530 << " exceeded the maximum FIDL message size and may not "
531 "be delivered to the accessibility manager service.";
532}
533
536 float view_pixel_ratio) {
537 if (update.empty()) {
538 return;
539 }
540 FML_DCHECK(nodes_.find(kRootNodeId) != nodes_.end() ||
541 update.find(kRootNodeId) != update.end())
542 << "AccessibilityBridge received an update with out ever getting a root "
543 "node.";
544
545 FuchsiaAtomicUpdate atomic_update;
546 bool has_root_node_update = false;
547 // TODO(MI4-2498): Actions, Roles, hit test children, additional
548 // flags/states/attr
549
550 // TODO(MI4-1478): Support for partial updates for nodes > 64kb
551 // e.g. if a node has a long label or more than 64k children.
552 for (const auto& [flutter_node_id, flutter_node] : update) {
553 size_t this_node_size = sizeof(fuchsia::accessibility::semantics::Node);
554 // We handle root update separately in GetRootNodeUpdate.
555 // TODO(chunhtai): remove this special case after we remove the inverse
556 // view pixel ratio transformation in scenic view.
557 // TODO(http://fxbug.dev/75908): Investigate flutter a11y bridge refactor
558 // after removal of the inverse view pixel ratio transformation in scenic
559 // view).
560 if (flutter_node.id == kRootNodeId) {
561 root_flutter_semantics_node_ = flutter_node;
562 has_root_node_update = true;
563 continue;
564 }
565 // Store the nodes for later hit testing and logging.
566 nodes_[flutter_node.id].data = flutter_node;
567
568 fuchsia::accessibility::semantics::Node fuchsia_node;
569 std::vector<uint32_t> child_ids;
570 // Send the nodes in traversal order, so the manager can figure out
571 // traversal.
572 for (int32_t flutter_child_id : flutter_node.childrenInTraversalOrder) {
573 child_ids.push_back(FlutterIdToFuchsiaId(flutter_child_id));
574 }
575 // TODO(http://fxbug.dev/75910): check the usage of FlutterIdToFuchsiaId in
576 // the flutter accessibility bridge.
577 fuchsia_node.set_node_id(flutter_node.id)
578 .set_role(GetNodeRole(flutter_node))
579 .set_location(GetNodeLocation(flutter_node))
580 .set_transform(GetNodeTransform(flutter_node))
581 .set_attributes(GetNodeAttributes(flutter_node, &this_node_size))
582 .set_states(GetNodeStates(flutter_node, &this_node_size))
583 .set_actions(GetNodeActions(flutter_node, &this_node_size))
584 .set_child_ids(child_ids);
585 this_node_size +=
586 kNodeIdSize * flutter_node.childrenInTraversalOrder.size();
587
588 atomic_update.AddNodeUpdate(std::move(fuchsia_node), this_node_size);
589 }
590
591 // Handles root node update.
592 if (has_root_node_update || last_seen_view_pixel_ratio_ != view_pixel_ratio) {
593 last_seen_view_pixel_ratio_ = view_pixel_ratio;
594 size_t root_node_size;
595 fuchsia::accessibility::semantics::Node root_update =
596 GetRootNodeUpdate(root_node_size);
597 atomic_update.AddNodeUpdate(std::move(root_update), root_node_size);
598 }
599
600 PruneUnreachableNodes(&atomic_update);
601 UpdateScreenRects();
602
603 atomic_updates_->push(std::move(atomic_update));
604 if (atomic_updates_->size() == 1) {
605 // There were no commits in the queue, so send this one.
606 Apply(&atomic_updates_->front());
607 }
608}
609
610fuchsia::accessibility::semantics::Node AccessibilityBridge::GetRootNodeUpdate(
611 size_t& node_size) {
612 fuchsia::accessibility::semantics::Node root_fuchsia_node;
613 std::vector<uint32_t> child_ids;
614 node_size = sizeof(fuchsia::accessibility::semantics::Node);
615 for (int32_t flutter_child_id :
616 root_flutter_semantics_node_.childrenInTraversalOrder) {
617 child_ids.push_back(FlutterIdToFuchsiaId(flutter_child_id));
618 }
619 // Applies the inverse view pixel ratio transformation to the root node.
620 float inverse_view_pixel_ratio = 1.f / last_seen_view_pixel_ratio_;
621 SkM44 inverse_view_pixel_ratio_transform;
622 inverse_view_pixel_ratio_transform.setScale(inverse_view_pixel_ratio,
623 inverse_view_pixel_ratio, 1.f);
624
625 SkM44 result = root_flutter_semantics_node_.transform *
626 inverse_view_pixel_ratio_transform;
627 nodes_[root_flutter_semantics_node_.id].data = root_flutter_semantics_node_;
628
629 // TODO(http://fxbug.dev/75910): check the usage of FlutterIdToFuchsiaId in
630 // the flutter accessibility bridge.
631 root_fuchsia_node.set_node_id(root_flutter_semantics_node_.id)
632 .set_role(GetNodeRole(root_flutter_semantics_node_))
633 .set_location(GetNodeLocation(root_flutter_semantics_node_))
634 .set_transform(ConvertSkiaTransformToMat4(result))
635 .set_attributes(
636 GetNodeAttributes(root_flutter_semantics_node_, &node_size))
637 .set_states(GetNodeStates(root_flutter_semantics_node_, &node_size))
638 .set_actions(GetNodeActions(root_flutter_semantics_node_, &node_size))
639 .set_child_ids(child_ids);
640 node_size += kNodeIdSize *
641 root_flutter_semantics_node_.childrenInTraversalOrder.size();
642 return root_fuchsia_node;
643}
644
646 fuchsia::accessibility::semantics::SemanticEvent semantic_event;
647 fuchsia::accessibility::semantics::AnnounceEvent announce_event;
648 announce_event.set_message(message);
649 semantic_event.set_announce(std::move(announce_event));
650
651 tree_ptr_->SendSemanticEvent(std::move(semantic_event), []() {});
652}
653
654void AccessibilityBridge::UpdateScreenRects() {
655 std::unordered_set<int32_t> visited_nodes;
656
657 // The embedder applies a special pixel ratio transform to the root of the
658 // view, and the accessibility bridge applies the inverse of this transform
659 // to the root node. However, this transform is not persisted in the flutter
660 // representation of the root node, so we need to account for it explicitly
661 // here.
662 float inverse_view_pixel_ratio = 1.f / last_seen_view_pixel_ratio_;
663 SkM44 inverse_view_pixel_ratio_transform;
664 inverse_view_pixel_ratio_transform.setScale(inverse_view_pixel_ratio,
665 inverse_view_pixel_ratio, 1.f);
666
667 UpdateScreenRects(kRootNodeId, inverse_view_pixel_ratio_transform,
668 &visited_nodes);
669}
670
671void AccessibilityBridge::UpdateScreenRects(
672 int32_t node_id,
673 SkM44 parent_transform,
674 std::unordered_set<int32_t>* visited_nodes) {
675 auto it = nodes_.find(node_id);
676 if (it == nodes_.end()) {
677 FML_LOG(ERROR) << "UpdateScreenRects called on unknown node";
678 return;
679 }
680 auto& node = it->second;
681 const auto& current_transform = parent_transform * node.data.transform;
682
683 const auto& rect = node.data.rect;
684 SkV4 dst[2] = {
685 current_transform.map(rect.left(), rect.top(), 0, 1),
686 current_transform.map(rect.right(), rect.bottom(), 0, 1),
687 };
688 node.screen_rect.setLTRB(dst[0].x, dst[0].y, dst[1].x, dst[1].y);
689 node.screen_rect.sort();
690
691 visited_nodes->emplace(node_id);
692
693 for (uint32_t child_id : node.data.childrenInHitTestOrder) {
694 if (visited_nodes->find(child_id) == visited_nodes->end()) {
695 UpdateScreenRects(child_id, current_transform, visited_nodes);
696 }
697 }
698}
699
700std::optional<flutter::SemanticsAction>
701AccessibilityBridge::GetFlutterSemanticsAction(
702 fuchsia::accessibility::semantics::Action fuchsia_action,
703 uint32_t node_id) {
704 switch (fuchsia_action) {
705 // The default action associated with the element.
706 case fuchsia::accessibility::semantics::Action::DEFAULT:
708 // The secondary action associated with the element. This may correspond to
709 // a long press (touchscreens) or right click (mouse).
710 case fuchsia::accessibility::semantics::Action::SECONDARY:
712 // Set (input/non-accessibility) focus on this element.
713 case fuchsia::accessibility::semantics::Action::SET_FOCUS:
714 FML_LOG(WARNING)
715 << "Unsupported action SET_FOCUS sent for accessibility node "
716 << node_id;
717 return {};
718 // Set the element's value.
719 case fuchsia::accessibility::semantics::Action::SET_VALUE:
720 FML_LOG(WARNING)
721 << "Unsupported action SET_VALUE sent for accessibility node "
722 << node_id;
723 return {};
724 // Scroll node to make it visible.
725 case fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN:
727 case fuchsia::accessibility::semantics::Action::INCREMENT:
729 case fuchsia::accessibility::semantics::Action::DECREMENT:
731 default:
732 FML_LOG(WARNING) << "Unexpected action "
733 << static_cast<int32_t>(fuchsia_action)
734 << " sent for accessibility node " << node_id;
735 return {};
736 }
737}
738
739// |fuchsia::accessibility::semantics::SemanticListener|
741 uint32_t node_id,
742 fuchsia::accessibility::semantics::Action action,
743 fuchsia::accessibility::semantics::SemanticListener::
744 OnAccessibilityActionRequestedCallback callback) {
745 // TODO(http://fxbug.dev/75910): check the usage of FlutterIdToFuchsiaId in
746 // the flutter accessibility bridge.
747 if (nodes_.find(node_id) == nodes_.end()) {
748 FML_LOG(ERROR) << "Attempted to send accessibility action "
749 << static_cast<int32_t>(action)
750 << " to unknown node id: " << node_id;
751 callback(false);
752 return;
753 }
754
755 std::optional<flutter::SemanticsAction> flutter_action =
756 GetFlutterSemanticsAction(action, node_id);
757 if (!flutter_action.has_value()) {
758 callback(false);
759 return;
760 }
761 dispatch_semantics_action_callback_(static_cast<int32_t>(node_id),
762 flutter_action.value());
763 callback(true);
764}
765
766// |fuchsia::accessibility::semantics::SemanticListener|
768 fuchsia::math::PointF local_point,
769 fuchsia::accessibility::semantics::SemanticListener::HitTestCallback
770 callback) {
771 auto hit_node_id = GetHitNode(kRootNodeId, local_point.x, local_point.y);
772 FML_DCHECK(hit_node_id.has_value());
773 fuchsia::accessibility::semantics::Hit hit;
774 // TODO(http://fxbug.dev/75910): check the usage of FlutterIdToFuchsiaId in
775 // the flutter accessibility bridge.
776 hit.set_node_id(hit_node_id.value_or(kRootNodeId));
777 callback(std::move(hit));
778}
779
780std::optional<int32_t> AccessibilityBridge::GetHitNode(int32_t node_id,
781 float x,
782 float y) {
783 auto it = nodes_.find(node_id);
784 if (it == nodes_.end()) {
785 FML_LOG(ERROR) << "Attempted to hit test unknown node id: " << node_id;
786 return {};
787 }
788 auto const& node = it->second;
789 if (node.data.flags.isHidden || !node.screen_rect.contains(x, y)) {
790 return {};
791 }
792 for (int32_t child_id : node.data.childrenInHitTestOrder) {
793 auto candidate = GetHitNode(child_id, x, y);
794 if (candidate) {
795 return candidate;
796 }
797 }
798
799 if (IsFocusable(node.data)) {
800 return node_id;
801 }
802
803 return {};
804}
805
806bool AccessibilityBridge::IsFocusable(
807 const flutter::SemanticsNode& node) const {
808 if (node.flags.scopesRoute) {
809 return false;
810 }
811
813 return true;
814 }
815
816 // Always consider platform views focusable.
817 if (node.IsPlatformViewNode()) {
818 return true;
819 }
820
821 // Always consider actionable nodes focusable.
822 if (node.actions != 0) {
823 return true;
824 }
825
826 // Consider text nodes focusable.
827 return !node.label.empty() || !node.value.empty() || !node.hint.empty();
828}
829
830// |fuchsia::accessibility::semantics::SemanticListener|
831void AccessibilityBridge::OnSemanticsModeChanged(
832 bool enabled,
833 OnSemanticsModeChangedCallback callback) {
834 set_semantics_enabled_callback_(enabled);
835}
836
837#if !FLUTTER_RELEASE
838void AccessibilityBridge::FillInspectTree(int32_t flutter_node_id,
839 int32_t current_level,
840 inspect::Node inspect_node,
841 inspect::Inspector* inspector) const {
842 const auto it = nodes_.find(flutter_node_id);
843 if (it == nodes_.end()) {
844 inspect_node.CreateString(
845 "missing_child",
846 "This node has a parent in the semantic tree but has no value",
847 inspector);
848 inspector->emplace(std::move(inspect_node));
849 return;
850 }
851 const auto& semantic_node = it->second;
852 const auto& data = semantic_node.data;
853
854 inspect_node.CreateInt("id", data.id, inspector);
855
856 // Even with an empty label, we still want to create the property to
857 // explicetly show that it is empty.
858 inspect_node.CreateString("label", data.label, inspector);
859 if (!data.hint.empty()) {
860 inspect_node.CreateString("hint", data.hint, inspector);
861 }
862 if (!data.value.empty()) {
863 inspect_node.CreateString("value", data.value, inspector);
864 }
865 if (!data.increasedValue.empty()) {
866 inspect_node.CreateString("increased_value", data.increasedValue,
867 inspector);
868 }
869 if (!data.decreasedValue.empty()) {
870 inspect_node.CreateString("decreased_value", data.decreasedValue,
871 inspector);
872 }
873
874 if (data.textDirection) {
875 inspect_node.CreateString(
876 "text_direction", data.textDirection == 1 ? "RTL" : "LTR", inspector);
877 }
878
879 std::string flags = NodeFlagsToString(data);
880 if (!flags.empty()) {
881 inspect_node.CreateString("flags", NodeFlagsToString(data), inspector);
882 }
883 if (data.actions) {
884 inspect_node.CreateString("actions", NodeActionsToString(data), inspector);
885 }
886
887 inspect_node.CreateString(
888 "location", NodeLocationToString(semantic_node.screen_rect), inspector);
889 if (!data.childrenInTraversalOrder.empty() ||
890 !data.childrenInHitTestOrder.empty()) {
891 inspect_node.CreateString("children", NodeChildrenToString(data),
892 inspector);
893 }
894
895 inspect_node.CreateInt("current_level", current_level, inspector);
896
897 for (int32_t flutter_child_id : semantic_node.data.childrenInTraversalOrder) {
898 const auto inspect_name = "node_" + std::to_string(flutter_child_id);
899 FillInspectTree(flutter_child_id, current_level + 1,
900 inspect_node.CreateChild(inspect_name), inspector);
901 }
902
903 inspector->emplace(std::move(inspect_node));
904}
905#endif // !FLUTTER_RELEASE
906
907void AccessibilityBridge::Apply(FuchsiaAtomicUpdate* atomic_update) {
908 size_t begin = 0;
909 auto it = atomic_update->deletions.begin();
910
911 // Process up to kMaxDeletionsPerUpdate deletions at a time.
912 while (it != atomic_update->deletions.end()) {
913 std::vector<uint32_t> to_delete;
914 size_t end = std::min(atomic_update->deletions.size() - begin,
916 std::copy(std::make_move_iterator(it), std::make_move_iterator(it + end),
917 std::back_inserter(to_delete));
918 tree_ptr_->DeleteSemanticNodes(std::move(to_delete));
919 begin = end;
920 it += end;
921 }
922
923 std::vector<fuchsia::accessibility::semantics::Node> to_update;
924 size_t current_size = 0;
925 for (auto& node_and_size : atomic_update->updates) {
926 if (current_size + node_and_size.second > kMaxMessageSize) {
927 tree_ptr_->UpdateSemanticNodes(std::move(to_update));
928 current_size = 0;
929 to_update.clear();
930 }
931 current_size += node_and_size.second;
932 to_update.push_back(std::move(node_and_size.first));
933 }
934 if (!to_update.empty()) {
935 tree_ptr_->UpdateSemanticNodes(std::move(to_update));
936 }
937
938 // Commit this update and subsequent ones; for flow control wait for a
939 // response between each commit.
940 tree_ptr_->CommitUpdates(
941 [this, atomic_updates = std::weak_ptr<std::queue<FuchsiaAtomicUpdate>>(
942 atomic_updates_)]() {
943 auto atomic_updates_ptr = atomic_updates.lock();
944 if (!atomic_updates_ptr) {
945 // The queue no longer exists, which means that is no longer
946 // necessary.
947 return;
948 }
949 // Removes the update that just went through.
950 atomic_updates_ptr->pop();
951 if (!atomic_updates_ptr->empty()) {
952 Apply(&atomic_updates_ptr->front());
953 }
954 });
955
956 atomic_update->deletions.clear();
957 atomic_update->updates.clear();
958}
959
960void AccessibilityBridge::FuchsiaAtomicUpdate::AddNodeUpdate(
961 fuchsia::accessibility::semantics::Node node,
962 size_t size) {
963 if (size > kMaxMessageSize) {
964 // TODO(MI4-2531, FIDL-718): Remove this
965 // This is defensive. If, despite our best efforts, we ended up with a node
966 // that is larger than the max fidl size, we send no updates.
967 PrintNodeSizeError(node.node_id());
968 return;
969 }
970 updates.emplace_back(std::move(node), size);
971}
972
973void AccessibilityBridge::FuchsiaAtomicUpdate::AddNodeDeletion(uint32_t id) {
974 deletions.push_back(id);
975}
976} // namespace flutter_runner
void AddSemanticsNodeUpdate(const flutter::SemanticsNodeUpdates update, float view_pixel_ratio)
std::function< void(bool)> SetSemanticsEnabledCallback
void OnAccessibilityActionRequested(uint32_t node_id, fuchsia::accessibility::semantics::Action action, fuchsia::accessibility::semantics::SemanticListener::OnAccessibilityActionRequestedCallback callback) override
void HitTest(fuchsia::math::PointF local_point, fuchsia::accessibility::semantics::SemanticListener::HitTestCallback callback) override
static constexpr uint32_t kMaxMessageSize
void RequestAnnounce(const std::string message)
std::function< void(int32_t, flutter::SemanticsAction)> DispatchSemanticsActionCallback
AccessibilityBridge(SetSemanticsEnabledCallback set_semantics_enabled_callback, DispatchSemanticsActionCallback dispatch_semantics_action_callback, fuchsia::accessibility::semantics::SemanticsManagerHandle semantics_manager, fuchsia::ui::views::ViewRef view_ref, inspect::Node inspect_node)
static constexpr size_t kMaxDeletionsPerUpdate
int32_t value
int32_t x
VkQueue queue
Definition main.cc:71
G_BEGIN_DECLS GBytes * message
FlutterDesktopBinaryReply callback
#define FML_LOG(severity)
Definition logging.h:101
#define FML_DCHECK(condition)
Definition logging.h:122
double y
static uint32_t FlutterIdToFuchsiaId(int32_t flutter_node_id)
static void PrintNodeSizeError(uint32_t node_id)
std::unordered_map< int32_t, SemanticsNode > SemanticsNodeUpdates
Definition ref_ptr.h:261
fidl::Binding< fuchsia::ui::composition::ChildViewWatcher > binding_
SemanticsCheckState isChecked
SemanticsTristate isFocused
SemanticsTristate isSelected
SemanticsTristate isToggled
SemanticsTristate isEnabled
std::vector< int32_t > childrenInHitTestOrder
bool HasAction(SemanticsAction action) const
bool IsPlatformViewNode() const
std::vector< int32_t > childrenInTraversalOrder
const size_t end
std::shared_ptr< const fml::Mapping > data