blob: 7b4b274ba6f05ebc87841d701d68f5e939f5c1c8 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromecast/browser/accessibility/flutter/ax_tree_source_flutter.h"
#include <stack>
#include "base/check_op.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "chromecast/browser/accessibility/accessibility_manager.h"
#include "chromecast/browser/accessibility/flutter/flutter_semantics_node_wrapper.h"
#include "chromecast/browser/cast_browser_process.h"
#include "chromecast/browser/cast_web_contents_observer.h"
#include "chromecast/browser/ui/aura/accessibility/automation_manager_aura.h"
#include "content/public/browser/tts_controller.h"
#include "content/public/browser/tts_utterance.h"
#include "extensions/browser/api/automation_internal/automation_event_router.h"
#include "extensions/browser/api/automation_internal/automation_event_router_interface.h"
#include "ui/accessibility/aura/aura_window_properties.h"
#include "ui/aura/window.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
namespace {
ax::mojom::Event ToAXEvent(
gallium::castos::OnAccessibilityEventRequest_EventType flutter_event_type) {
switch (flutter_event_type) {
case gallium::castos::OnAccessibilityEventRequest_EventType_FOCUSED:
return ax::mojom::Event::kFocus;
case gallium::castos::OnAccessibilityEventRequest_EventType_CLICKED:
case gallium::castos::OnAccessibilityEventRequest_EventType_LONG_CLICKED:
return ax::mojom::Event::kClicked;
case gallium::castos::OnAccessibilityEventRequest_EventType_TEXT_CHANGED:
return ax::mojom::Event::kTextChanged;
case gallium::castos::
OnAccessibilityEventRequest_EventType_TEXT_SELECTION_CHANGED:
return ax::mojom::Event::kTextSelectionChanged;
case gallium::castos::OnAccessibilityEventRequest_EventType_HOVER_ENTER:
return ax::mojom::Event::kHover;
case gallium::castos::OnAccessibilityEventRequest_EventType_SCROLLED:
return ax::mojom::Event::kScrollPositionChanged;
case gallium::castos::OnAccessibilityEventRequest_EventType_CONTENT_CHANGED:
return ax::mojom::Event::kChildrenChanged;
case gallium::castos::
OnAccessibilityEventRequest_EventType_WINDOW_STATE_CHANGED:
return ax::mojom::Event::kLayoutComplete;
default:
return ax::mojom::Event::kNone;
}
}
} // namespace
namespace chromecast {
namespace accessibility {
AXTreeSourceFlutter::AXTreeWebContentsObserver::AXTreeWebContentsObserver(
content::WebContents* web_contents,
AXTreeSourceFlutter* ax_tree_source)
: WebContentsObserver(web_contents), ax_tree_source_(ax_tree_source) {}
void AXTreeSourceFlutter::AXTreeWebContentsObserver::RenderFrameHostChanged(
content::RenderFrameHost* old_host,
content::RenderFrameHost* new_host) {
ax_tree_source_->UpdateTree();
}
void AXTreeSourceFlutter::AXTreeWebContentsObserver::
AXTreeIDForMainFrameHasChanged() {
ax_tree_source_->UpdateTree();
}
constexpr int kInvalidId = -1;
AXTreeSourceFlutter::AXTreeSourceFlutter(
Delegate* delegate,
content::BrowserContext* browser_context,
extensions::AutomationEventRouterInterface* event_router)
: current_tree_serializer_(std::make_unique<AXTreeFlutterSerializer>(this)),
root_id_(kInvalidId),
window_id_(kInvalidId),
focused_id_(kInvalidId),
delegate_(delegate),
browser_context_(browser_context),
event_router_(event_router
? event_router
: extensions::AutomationEventRouter::GetInstance()),
cast_web_contents_(nullptr),
accessibility_enabled_(false) {
DCHECK(delegate_);
}
AXTreeSourceFlutter::~AXTreeSourceFlutter() {
Reset();
}
void AXTreeSourceFlutter::NotifyAccessibilityEvent(
const gallium::castos::OnAccessibilityEventRequest* event_data) {
DCHECK(event_data);
if (event_data->node_data_size() > 0) {
// Remember most recent tree in case we need to update parents
// of child trees with new ax tree ids (i.e. due to embedded webview
// navigation)
last_event_data_ = *event_data;
}
// First find out if we know what to do with this event type from flutter.
if (event_data->event_type() ==
gallium::castos::OnAccessibilityEventRequest_EventType_ANNOUNCEMENT) {
if (!event_data->has_text())
return;
SubmitTTS(event_data->text());
return;
}
ax::mojom::Event translated_event = ToAXEvent(event_data->event_type());
if (translated_event == ax::mojom::Event::kNone) {
LOG(INFO) << "Ignoring unknown flutter ax event "
<< event_data->event_type() << ". No mapping available.";
return;
}
// b/150992421 - We sometimes get nodes that have been reparented.
// Any node that is reparented must first be deleted from its old
// parent before appearing under a new one. The tree serializer
// should be handling this case for us but isn't so the tree
// update fails. As a workaround until this is fixed upstream, we
// will identify these nodes ourselves and provide a separate update
// that will delete them prior to the full update.
reparented_children_.clear();
std::vector<int32_t> parents_with_deleted_children;
for (int i = 0; i < event_data->node_data_size(); ++i) {
const SemanticsNode& node = event_data->node_data(i);
for (int j = 0; j < node.child_node_ids_size(); ++j) {
int child_id = node.child_node_ids(j);
auto it = parent_map_.find(child_id);
if (it != parent_map_.end()) {
if (node.node_id() != it->second) {
// Remember this child and who its parent was.
reparented_children_.push_back(child_id);
parents_with_deleted_children.push_back(it->second);
}
}
}
}
tree_map_.clear();
cached_computed_bounds_.clear();
if (event_data->node_data_size() > 0) {
// Unless there are new nodes, don't clear previous parent map so we
// can detect reparenting above.
parent_map_.clear();
}
window_id_ = event_data->window_id();
// The following loops perform caching to prepare for AXTreeSerializer.
// First, we need to cache parent links, which are implied by a node's child
// ids. Next, we cache the nodes by id. During this process, we can detect
// the root node based upon the parent links we cached above. Finally, we
// cache each node's computed bounds, based on its descendants.
std::map<int32_t, int32_t> all_parent_map;
std::map<int32_t, std::vector<int32_t>> all_children_map;
for (int i = 0; i < event_data->node_data_size(); ++i) {
const SemanticsNode& node = event_data->node_data(i);
for (int j = 0; j < node.child_node_ids_size(); ++j) {
all_children_map[node.node_id()].push_back(node.child_node_ids(j));
all_parent_map[node.child_node_ids(j)] = node.node_id();
}
}
// Now copy just the relevant subtree containing the source_id into the
// |parent_map_|.
root_id_ = event_data->source_id();
// Walk up to the root from the source_id.
for (auto it = all_parent_map.find(root_id_); it != all_parent_map.end();
it = all_parent_map.find(root_id_)) {
root_id_ = it->second;
}
// Walk back down through children map to populate parent_map_.
std::stack<int32_t> stack;
stack.push(root_id_);
while (!stack.empty()) {
int32_t parent = stack.top();
stack.pop();
for (int32_t child_id : all_children_map[parent]) {
parent_map_[child_id] = parent;
stack.push(child_id);
}
}
std::vector<std::string> new_child_trees;
for (int i = 0; i < event_data->node_data_size(); ++i) {
int32_t id = event_data->node_data(i).node_id();
// Only map nodes in the parent_map and the root.
// This avoids adding other subtrees that are not interesting.
if (parent_map_.find(id) == parent_map_.end() && id != root_id_)
continue;
const SemanticsNode& node = event_data->node_data(i);
tree_map_[id] = std::make_unique<FlutterSemanticsNodeWrapper>(this, &node);
if (tree_map_[id]->IsFocused()) {
focused_id_ = id;
}
// Place focus on the node that holds a child tree. When the
// child tree is removed, focus will be placed back on the root.
if (node.has_plugin_id()) {
std::string ax_tree_id = node.plugin_id();
new_child_trees.push_back(ax_tree_id);
if (std::find(child_trees_.begin(), child_trees_.end(), ax_tree_id) ==
child_trees_.end()) {
if (ax_tree_id.rfind("T:", 0) == 0) {
int web_contents_id;
base::StringToInt(ax_tree_id.substr(2), &web_contents_id);
std::vector<CastWebContents*> all_contents =
CastWebContents::GetAll();
for (CastWebContents* contents : all_contents) {
if (contents->id() == web_contents_id &&
child_tree_observers_.find(web_contents_id) ==
child_tree_observers_.end()) {
child_tree_observers_[contents->id()] = std::make_unique<
AXTreeSourceFlutter::AXTreeWebContentsObserver>(
contents->web_contents(), this);
CastWebContentsObserver::Observe(contents);
cast_web_contents_ = contents;
break;
}
}
}
}
focused_id_ = node.node_id();
}
}
// Do we need to put focus somewhere after a child tree
// has been removed?
bool need_focus_clear = false;
for (std::string id : child_trees_) {
// Is this old child tree still known?
if (std::find(new_child_trees.begin(), new_child_trees.end(), id) ==
new_child_trees.end()) {
// No, clear focus.
need_focus_clear = true;
focused_id_ = root_id_;
break;
}
}
child_trees_ = new_child_trees;
// Assuming |nodeData| is in pre-order, compute cached bounds in post-order to
// avoid an O(n^2) amount of work as the computed bounds uses descendant
// bounds.
for (int i = 0; i < event_data->node_data_size(); ++i) {
int32_t id = event_data->node_data(i).node_id();
if (parent_map_.find(id) == parent_map_.end() && id != root_id_)
continue;
cached_computed_bounds_[id] = ComputeEnclosingBounds(tree_map_[id].get());
}
// If focus was not set from above, set it now on the root node.
if (focused_id_ < 0 && root_id_ >= 0) {
focused_id_ = root_id_;
}
std::vector<ui::AXEvent> events;
ui::AXEvent event;
event.event_type = translated_event;
event.id = event_data->source_id();
events.push_back(std::move(event));
if (event_data->event_type() ==
gallium::castos::OnAccessibilityEventRequest_EventType_CONTENT_CHANGED) {
current_tree_serializer_->InvalidateSubtree(
GetFromId(event_data->source_id()));
}
std::vector<ui::AXTreeUpdate> updates;
if (event_data->event_type() !=
gallium::castos::OnAccessibilityEventRequest_EventType_HOVER_ENTER &&
event_data->event_type() !=
gallium::castos::OnAccessibilityEventRequest_EventType_HOVER_EXIT) {
// For every parent whose child has been moved, serialize an update.
// This update will filter all the children that have moved.
for (int32_t nid : parents_with_deleted_children) {
ui::AXTreeUpdate update;
current_tree_serializer_->SerializeChanges(GetFromId(nid), &update);
updates.push_back(std::move(update));
}
// If there were any children that were reparented, invalidate the entire
// tree so the new parents get the children.
if (reparented_children_.size() > 0) {
current_tree_serializer_->InvalidateSubtree(GetFromId(root_id_));
}
// Clear reparented children.
reparented_children_.clear();
// Handle routes added/removed from the tree.
HandleRoutes(&events);
ui::AXTreeUpdate update;
current_tree_serializer_->SerializeChanges(
GetFromId(event_data->source_id()), &update);
updates.push_back(std::move(update));
HandleLiveRegions(&events);
// b/162311902: For nodes that have scroll extents, rapidly changing the
// value will result in queueing up the values and speak out one by one.
// Here we handle the tts natively.
HandleNativeTTS();
HandleVirtualKeyboardNodes();
}
// Need to refocus
if (need_focus_clear) {
ui::AXEvent focus_event;
focus_event.event_type = ax::mojom::Event::kFocus;
focus_event.id = focused_id_;
focus_event.event_from = ax::mojom::EventFrom::kNone;
focus_event.event_from_action = ax::mojom::Action::kNone;
events.push_back(std::move(focus_event));
}
if (event_router_) {
event_router_->DispatchAccessibilityEvents(ax_tree_id(), std::move(updates),
gfx::Point(), std::move(events));
}
}
void AXTreeSourceFlutter::NotifyActionResult(const ui::AXActionData& data,
bool result) {
if (!event_router_)
return;
event_router_->DispatchActionResult(data, result, browser_context_);
}
bool AXTreeSourceFlutter::GetTreeData(ui::AXTreeData* data) const {
DCHECK(data);
data->tree_id = ax_tree_id();
if (focused_id_ >= 0) {
data->focus_id = focused_id_;
} else if (root_id_ >= 0) {
data->focus_id = root_id_;
}
return true;
}
FlutterSemanticsNode* AXTreeSourceFlutter::GetRoot() const {
return GetFromId(root_id_);
}
FlutterSemanticsNode* AXTreeSourceFlutter::GetFromId(int32_t id) const {
auto it = tree_map_.find(id);
if (it == tree_map_.end())
return nullptr;
return it->second.get();
}
int32_t AXTreeSourceFlutter::GetId(FlutterSemanticsNode* info_data) const {
if (!info_data)
return kInvalidId;
return info_data->GetId();
}
void AXTreeSourceFlutter::GetChildren(
FlutterSemanticsNode* info_data,
std::vector<FlutterSemanticsNode*>* out_children) const {
if (!info_data)
return;
info_data->GetChildren(out_children);
if (out_children->empty())
return;
// Filter out any reparented children so the update doesn't see them.
auto it = out_children->begin();
while (it != out_children->end()) {
if (std::find(reparented_children_.begin(), reparented_children_.end(),
(*it)->GetId()) != reparented_children_.end()) {
it = out_children->erase(it);
} else {
++it;
}
}
}
FlutterSemanticsNode* AXTreeSourceFlutter::GetParent(
FlutterSemanticsNode* info_data) const {
if (!info_data)
return nullptr;
auto it = parent_map_.find(info_data->GetId());
if (it != parent_map_.end())
return GetFromId(it->second);
return nullptr;
}
bool AXTreeSourceFlutter::IsValid(FlutterSemanticsNode* info_data) const {
return info_data;
}
bool AXTreeSourceFlutter::IsIgnored(FlutterSemanticsNode* info_data) const {
return false;
}
bool AXTreeSourceFlutter::IsEqual(FlutterSemanticsNode* info_data1,
FlutterSemanticsNode* info_data2) const {
if (!info_data1 || !info_data2)
return false;
return info_data1->GetId() == info_data2->GetId();
}
FlutterSemanticsNode* AXTreeSourceFlutter::GetNull() const {
return nullptr;
}
void AXTreeSourceFlutter::SerializeNode(FlutterSemanticsNode* info_data,
ui::AXNodeData* out_data) const {
if (!info_data)
return;
int32_t id = info_data->GetId();
out_data->id = id;
if (id == root_id_) {
out_data->role = ax::mojom::Role::kRootWebArea;
} else {
info_data->PopulateAXRole(out_data);
}
info_data->Serialize(out_data);
}
gfx::Rect AXTreeSourceFlutter::ComputeEnclosingBounds(
FlutterSemanticsNode* info_data) const {
gfx::Rect computed_bounds;
// Exit early if the node or window is invisible.
if (!info_data->IsVisibleToUser())
return computed_bounds;
ComputeEnclosingBoundsInternal(info_data, &computed_bounds);
return computed_bounds;
}
void AXTreeSourceFlutter::ComputeEnclosingBoundsInternal(
FlutterSemanticsNode* info_data,
gfx::Rect* computed_bounds) const {
auto cached_bounds = cached_computed_bounds_.find(info_data->GetId());
if (cached_bounds != cached_computed_bounds_.end()) {
computed_bounds->Union(cached_bounds->second);
return;
}
if (!info_data->IsVisibleToUser())
return;
if (info_data->CanBeAccessibilityFocused()) {
// Only consider nodes that can possibly be accessibility focused.
computed_bounds->Union(info_data->GetBounds());
return;
}
std::vector<FlutterSemanticsNode*> children;
info_data->GetChildren(&children);
if (children.empty())
return;
for (FlutterSemanticsNode* child : children)
ComputeEnclosingBoundsInternal(child, computed_bounds);
}
void AXTreeSourceFlutter::PerformAction(const ui::AXActionData& data) {
delegate_->OnAction(data);
}
void AXTreeSourceFlutter::Reset() {
tree_map_.clear();
parent_map_.clear();
cached_computed_bounds_.clear();
current_tree_serializer_ = std::make_unique<AXTreeFlutterSerializer>(this);
root_id_ = kInvalidId;
focused_id_ = kInvalidId;
if (!event_router_)
return;
event_router_->DispatchTreeDestroyedEvent(ax_tree_id(), browser_context_);
}
void AXTreeSourceFlutter::HandleVirtualKeyboardNodes() {
gfx::Rect bounds;
for (const auto& it : tree_map_) {
FlutterSemanticsNode* node_info = it.second.get();
if (!node_info->IsKeyboardNode())
continue;
bounds.Union(node_info->GetBounds());
}
if (bounds != keyboard_bounds_) {
keyboard_bounds_ = bounds;
delegate_->OnVirtualKeyboardBoundsChange(keyboard_bounds_);
}
}
void AXTreeSourceFlutter::HandleNativeTTS() {
std::map<int32_t, std::string> new_native_tts_name_cache;
// Cache current native tts name cache.
for (const auto& it : tree_map_) {
FlutterSemanticsNode* node_info = it.second.get();
if (!node_info->IsRapidChangingSlider())
continue;
std::vector<std::string> names;
if (node_info->HasLabelHint()) {
names.push_back(node_info->GetLabelHint());
}
if (node_info->HasValue()) {
names.push_back(node_info->GetValue());
}
new_native_tts_name_cache[node_info->GetId()] =
base::JoinString(names, " ");
}
// Compare to the previous one, and send out TTS if needed.
for (const auto& it : new_native_tts_name_cache) {
auto prev_it = native_tts_name_cache_.find(it.first);
if (prev_it != native_tts_name_cache_.end() &&
prev_it->second != it.second) {
// Send to TTS controller.
SubmitTTS(it.second);
}
}
std::swap(native_tts_name_cache_, new_native_tts_name_cache);
}
void AXTreeSourceFlutter::HandleLiveRegions(std::vector<ui::AXEvent>* events) {
std::map<int32_t, std::string> new_live_region_map;
// Cache current live region's name.
for (const auto& it : tree_map_) {
FlutterSemanticsNode* node_info = it.second.get();
if (!node_info->IsLiveRegion())
continue;
std::stack<FlutterSemanticsNode*> stack;
stack.push(node_info);
while (!stack.empty()) {
FlutterSemanticsNode* node = stack.top();
stack.pop();
DCHECK(node);
ui::AXNodeData data;
SerializeNode(node, &data);
std::string name;
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name);
new_live_region_map[node->GetId()] = name;
std::vector<FlutterSemanticsNode*> children;
node->GetChildren(&children);
for (FlutterSemanticsNode* child : children)
stack.push(child);
}
}
// Compare to the previous one, and add an event if needed.
for (const auto& it : new_live_region_map) {
auto prev_it = live_region_name_cache_.find(it.first);
if (prev_it == live_region_name_cache_.end())
continue;
if (prev_it->second != it.second) {
events->emplace_back();
ui::AXEvent& event = events->back();
event.event_type = ax::mojom::Event::kLiveRegionChanged;
event.id = it.first;
}
}
std::swap(live_region_name_cache_, new_live_region_map);
}
// Handle created/deleted nodes with scopes routes flag set.
void AXTreeSourceFlutter::HandleRoutes(std::vector<ui::AXEvent>* events) {
bool focused_new = false;
for (const auto& it : tree_map_) {
FlutterSemanticsNode* node = it.second.get();
if (!node->HasScopesRoute())
continue;
// Do we know about this node already? If so, skip.
if (std::find(scopes_route_cache_.begin(), scopes_route_cache_.end(),
node->GetId()) != scopes_route_cache_.end()) {
continue;
}
// Find a node in the sub-tree with names route flag set.
FlutterSemanticsNode* sub_node = FindRoutesNode(node);
if (sub_node) {
// Only register the scopes route node in our cache
// if a names route is found.
scopes_route_cache_.push_back(node->GetId());
ui::AXNodeData data;
SerializeNode(sub_node, &data);
std::string name;
data.GetStringAttribute(ax::mojom::StringAttribute::kName, &name);
if (name.length() > 0) {
focused_new = true;
// Focus the node.
focused_id_ = sub_node->GetId();
events->emplace_back();
ui::AXEvent& focus_event = events->back();
focus_event.event_type = ax::mojom::Event::kFocus;
focus_event.id = focused_id_;
focus_event.event_from = ax::mojom::EventFrom::kNone;
focus_event.event_from_action = ax::mojom::Action::kNone;
}
}
}
// Clean up the cache for those nodes that should not be in the cache anymore.
bool need_refocus = false;
for (std::vector<int32_t>::iterator it = scopes_route_cache_.begin();
it != scopes_route_cache_.end();) {
int32_t id = *it;
FlutterSemanticsNode* scopes_route_node = GetFromId(id);
if (scopes_route_node == nullptr || !scopes_route_node->HasScopesRoute() ||
FindRoutesNode(scopes_route_node) == nullptr) {
// This was removed in the latest tree update or there are no more
// RoutesNode child.
it = scopes_route_cache_.erase(it++);
need_refocus = true;
} else {
it++;
}
}
// After a deletion, use the last scopes route node to refocus on a child
// with names route set (unless we already focused on a new node from above).
if (need_refocus && !focused_new) {
// Select the last in-depth node with scopesRoute for refocus
FlutterSemanticsNode* refocused_routes_node = nullptr;
if (scopes_route_cache_.size() > 0)
refocused_routes_node =
FindRoutesNode(GetFromId(scopes_route_cache_.back()));
if (refocused_routes_node) {
focused_id_ = refocused_routes_node->GetId();
} else {
focused_id_ = FindFirstFocusableNodeId();
}
events->emplace_back();
ui::AXEvent& focus_event = events->back();
focus_event.event_type = ax::mojom::Event::kFocus;
focus_event.id = focused_id_;
focus_event.event_from = ax::mojom::EventFrom::kNone;
focus_event.event_from_action = ax::mojom::Action::kNone;
}
}
// Perform depth first search for a subtree node under 'parent'
// with names route flag set.
FlutterSemanticsNode* AXTreeSourceFlutter::FindRoutesNode(
FlutterSemanticsNode* parent) {
if (parent == nullptr)
return nullptr;
std::stack<FlutterSemanticsNode*> stack;
stack.push(parent);
while (!stack.empty()) {
FlutterSemanticsNode* node = stack.top();
stack.pop();
DCHECK(node);
if (node->HasNamesRoute()) {
return node;
}
std::vector<FlutterSemanticsNode*> children;
node->GetChildren(&children);
for (FlutterSemanticsNode* child : children)
stack.push(child);
}
return nullptr;
}
// Find the first focusable node.
int32_t AXTreeSourceFlutter::FindFirstFocusableNodeId() {
std::stack<FlutterSemanticsNode*> stack;
stack.push(GetFromId(root_id_));
while (!stack.empty()) {
FlutterSemanticsNode* node = stack.top();
stack.pop();
DCHECK(node);
if (node->CanBeAccessibilityFocused()) {
return node->GetId();
}
std::vector<FlutterSemanticsNode*> children;
node->GetChildren(&children);
for (FlutterSemanticsNode* child : children)
stack.push(child);
}
// Fallback to root if none found.
return root_id_;
}
void AXTreeSourceFlutter::SubmitTTS(const std::string& text) {
if (!accessibility_enabled_)
return;
std::unique_ptr<content::TtsUtterance> utterance =
content::TtsUtterance::Create(browser_context_);
utterance->SetText(text);
auto* tts_controller = content::TtsController::GetInstance();
tts_controller->Stop();
tts_controller->SpeakOrEnqueue(std::move(utterance));
}
void AXTreeSourceFlutter::UpdateTree() {
// Update the tree with last known flutter nodes.
// TODO: A more efficient update would be to isolate just the parent node
// whose child has changed. Consider giving the node to the observer
// class on creation and passing through here.
NotifyAccessibilityEvent(&last_event_data_);
}
void AXTreeSourceFlutter::PageStopped(PageState page_state, int error_code) {
// Webview is gone. Stop observing.
CastWebContentsObserver::Observe(nullptr);
child_tree_observers_.erase(cast_web_contents_->id());
cast_web_contents_ = nullptr;
}
void AXTreeSourceFlutter::SetAccessibilityEnabled(bool value) {
accessibility_enabled_ = value;
}
} // namespace accessibility
} // namespace chromecast