Commit 026da6f9 authored by Hiroki Sato's avatar Hiroki Sato Committed by Commit Bot

arc-a11y: Stabilize accessibility focus in ChromeVox + ARC++

This CL adds heuristic logics in ARC accessibility to stabilize the
focus.

* On accessibility focus changed event from Android, update the focus
  stored in AXTreeSourceArc to the event source. TalkBack updates the
  accessibility focus on this event. Also by doing this, we can follow
  the accessibility foucsed node in a case like b:130186502.

* AXTreeSourceArc stores the previously focused node for each root
  window. By doing this, when a user open a ChromeVox menu or another
  window and comes back to the app, the focus remains on the same node.

AX-Relnotes: Improved accessibility focus handling in ARC apps.
Bug: 130186502
Bug: 151398335
Test: manual. described above. Also unit_tests.
Change-Id: Ib002f559569409bc5ef4ab8982174d61a096a3b6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2398438
Commit-Queue: Hiroki Sato <hirokisato@chromium.org>
Reviewed-by: default avatarDavid Tseng <dtseng@chromium.org>
Reviewed-by: default avatarSara Kato <sarakato@chromium.org>
Cr-Commit-Position: refs/heads/master@{#806846}
parent 78f2ec8b
......@@ -70,7 +70,7 @@ bool AccessibilityNodeInfoDataWrapper::IsVirtualNode() const {
}
bool AccessibilityNodeInfoDataWrapper::IsIgnored() const {
if (!tree_source_->IsScreenReaderMode())
if (!tree_source_->UseFullFocusMode())
return !IsImportantInAndroid();
if (!IsImportantInAndroid() || !HasImportantProperty())
......@@ -294,7 +294,7 @@ void AccessibilityNodeInfoDataWrapper::PopulateAXState(
#undef MAP_STATE
const bool focusable = tree_source_->IsScreenReaderMode()
const bool focusable = tree_source_->UseFullFocusMode()
? IsAccessibilityFocusableContainer()
: GetProperty(AXBooleanProperty::FOCUSABLE);
if (focusable)
......@@ -537,7 +537,7 @@ std::string AccessibilityNodeInfoDataWrapper::ComputeAXName(
// If a node is accessibility focusable, but has no name, the name should be
// computed from its descendants.
if (names.empty() && tree_source_->IsScreenReaderMode() &&
if (names.empty() && tree_source_->UseFullFocusMode() &&
IsAccessibilityFocusableContainer())
ComputeNameFromContents(&names);
......
......@@ -119,18 +119,16 @@ class AccessibilityNodeInfoDataWrapperTest : public testing::Test,
}
// AXTreeSourceArc::Delegate overrides.
bool IsScreenReaderEnabled() const override { return screen_reader_enabled_; }
bool UseFullFocusMode() const override { return full_focus_mode_; }
void OnAction(const ui::AXActionData& data) const override {}
void set_screen_reader_mode(bool enabled) {
screen_reader_enabled_ = enabled;
}
void set_full_focus_mode(bool enabled) { full_focus_mode_ = enabled; }
AXTreeSourceArc* tree_source() { return tree_source_.get(); }
private:
const std::unique_ptr<TestAXTreeSourceArc> tree_source_;
bool screen_reader_enabled_ = true;
bool full_focus_mode_ = true;
};
TEST_F(AccessibilityNodeInfoDataWrapperTest, Name) {
......@@ -219,7 +217,7 @@ TEST_F(AccessibilityNodeInfoDataWrapperTest, NameFromDescendants) {
SetProperty(&child2, AXStringProperty::TEXT, "child2 label text");
// If the screen reader mode is off, do not compute from descendants.
set_screen_reader_mode(false);
set_full_focus_mode(false);
ui::AXNodeData data = CallSerialize(root_wrapper);
std::string name;
......@@ -240,7 +238,7 @@ TEST_F(AccessibilityNodeInfoDataWrapperTest, NameFromDescendants) {
// Enable screen reader.
// Compute the name of the clickable node from descendants, and ignore them.
set_screen_reader_mode(true);
set_full_focus_mode(true);
data = CallSerialize(root_wrapper);
ASSERT_TRUE(
......
......@@ -517,8 +517,8 @@ void ArcAccessibilityHelperBridge::OnAction(
base::Unretained(this), data));
}
bool ArcAccessibilityHelperBridge::IsScreenReaderEnabled() const {
return is_screen_reader_enabled_;
bool ArcAccessibilityHelperBridge::UseFullFocusMode() const {
return use_full_focus_mode_;
}
void ArcAccessibilityHelperBridge::OnTaskDestroyed(int32_t task_id) {
......@@ -729,8 +729,8 @@ void ArcAccessibilityHelperBridge::UpdateEnabledFeature() {
is_focus_highlight_enabled_ =
accessibility_manager->IsFocusHighlightEnabled();
is_screen_reader_enabled_ = accessibility_manager->IsSwitchAccessEnabled() ||
accessibility_manager->IsSpokenFeedbackEnabled();
use_full_focus_mode_ = accessibility_manager->IsSwitchAccessEnabled() ||
accessibility_manager->IsSpokenFeedbackEnabled();
bool add_activation_observer =
filter_type_ == arc::mojom::AccessibilityFilterType::ALL;
......
......@@ -98,7 +98,7 @@ class ArcAccessibilityHelperBridge
// AXTreeSourceArc::Delegate overrides.
void OnAction(const ui::AXActionData& data) const override;
bool IsScreenReaderEnabled() const override;
bool UseFullFocusMode() const override;
// ArcAppListPrefs::Observer overrides.
void OnTaskDestroyed(int32_t task_id) override;
......@@ -178,7 +178,7 @@ class ArcAccessibilityHelperBridge
bool activation_observer_added_ = false;
bool is_focus_highlight_enabled_ = false;
bool is_screen_reader_enabled_ = false;
bool use_full_focus_mode_ = false;
Profile* const profile_;
ArcBridgeService* const arc_bridge_service_;
TreeMap trees_;
......
......@@ -121,6 +121,8 @@ base::Optional<mojom::AccessibilityActionType> ConvertToAndroidAction(
case ax::mojom::Action::kDoDefault:
return arc::mojom::AccessibilityActionType::CLICK;
case ax::mojom::Action::kFocus:
// Fallthrough
case ax::mojom::Action::kSetSequentialFocusNavigationStartingPoint:
return arc::mojom::AccessibilityActionType::ACCESSIBILITY_FOCUS;
case ax::mojom::Action::kScrollToMakeVisible:
return arc::mojom::AccessibilityActionType::SHOW_ON_SCREEN;
......
......@@ -81,8 +81,8 @@ void AXTreeSourceArc::NotifyGetTextLocationDataResult(
GetAutomationEventRouter()->DispatchGetTextLocationDataResult(data, rect);
}
bool AXTreeSourceArc::IsScreenReaderMode() const {
return delegate_->IsScreenReaderEnabled();
bool AXTreeSourceArc::UseFullFocusMode() const {
return delegate_->UseFullFocusMode();
}
void AXTreeSourceArc::InvalidateTree() {
......@@ -376,6 +376,7 @@ AXTreeSourceArc::GetSelectedNodeInfoFromAdapterView(
}
bool AXTreeSourceArc::UpdateAndroidFocusedId(const AXEventData& event_data) {
// TODO(hirokisato): Handle CLEAR_ACCESSIBILITY_FOCUS event.
if (event_data.event_type == AXEventType::VIEW_FOCUSED) {
AccessibilityInfoDataWrapper* source_node = GetFromId(event_data.source_id);
if (source_node && source_node->IsVisibleToUser()) {
......@@ -385,6 +386,11 @@ bool AXTreeSourceArc::UpdateAndroidFocusedId(const AXEventData& event_data) {
if (IsValid(adjusted_node))
android_focused_id_ = adjusted_node->GetId();
}
} else if (event_data.event_type == AXEventType::VIEW_ACCESSIBILITY_FOCUSED &&
UseFullFocusMode()) {
AccessibilityInfoDataWrapper* source_node = GetFromId(event_data.source_id);
if (source_node && source_node->IsVisibleToUser())
android_focused_id_ = source_node->GetId();
} else if (event_data.event_type == AXEventType::VIEW_SELECTED) {
// In Android, VIEW_SELECTED event is dispatched in the two cases below:
// 1. Changing a value in ProgressBar or TimePicker in ARC P.
......@@ -411,8 +417,23 @@ bool AXTreeSourceArc::UpdateAndroidFocusedId(const AXEventData& event_data) {
// The event of WINDOW_STATE_CHANGED is fired only once for each window
// change and use it as a trigger to move the a11y focus to the first node.
AccessibilityInfoDataWrapper* source_node = GetFromId(event_data.source_id);
AccessibilityInfoDataWrapper* new_focus =
FindFirstFocusableNode(source_node);
AccessibilityInfoDataWrapper* new_focus = nullptr;
// If the current window has ever been visited in the current task, try
// focus on the last focus node in this window.
// We do it for WINDOW_STATE_CHANGED event from a window or a root node.
bool from_root_or_window = (source_node && !source_node->IsNode()) ||
IsRootOfNodeTree(event_data.source_id);
auto itr = root_window_id_to_last_focus_node_id_.find(*root_id_);
if (from_root_or_window &&
itr != root_window_id_to_last_focus_node_id_.end()) {
new_focus = GetFromId(itr->second);
}
// Otherwise, try focus on the first focusable node.
if (!IsValid(new_focus))
new_focus = FindFirstFocusableNode(GetFromId(event_data.source_id));
if (IsValid(new_focus))
android_focused_id_ = new_focus->GetId();
}
......@@ -423,6 +444,12 @@ bool AXTreeSourceArc::UpdateAndroidFocusedId(const AXEventData& event_data) {
android_focused_id_ = root_id_;
}
if (android_focused_id_.has_value()) {
root_window_id_to_last_focus_node_id_[*root_id_] = *android_focused_id_;
} else {
root_window_id_to_last_focus_node_id_.erase(*root_id_);
}
AccessibilityInfoDataWrapper* focused_node =
android_focused_id_.has_value() ? GetFromId(*android_focused_id_)
: nullptr;
......
......@@ -42,7 +42,7 @@ class AXTreeSourceArc : public ui::AXTreeSource<AccessibilityInfoDataWrapper*,
class Delegate {
public:
virtual void OnAction(const ui::AXActionData& data) const = 0;
virtual bool IsScreenReaderEnabled() const = 0;
virtual bool UseFullFocusMode() const = 0;
};
AXTreeSourceArc(Delegate* delegate, float device_scale_factor);
......@@ -64,7 +64,9 @@ class AXTreeSourceArc : public ui::AXTreeSource<AccessibilityInfoDataWrapper*,
// When it is enabled, this class exposes an accessibility tree optimized for
// screen readers such as ChromeVox and SwitchAccess. This intends to have the
// navigation order and focusabilities similar to TalkBack.
bool IsScreenReaderMode() const;
// Also, when it is enabled, the accessibility focus in Android is exposed as
// the focus of this tree.
bool UseFullFocusMode() const;
// Returns true if the node id is the root of the node tree (which can have a
// parent window).
......@@ -121,10 +123,14 @@ class AXTreeSourceArc : public ui::AXTreeSource<AccessibilityInfoDataWrapper*,
AccessibilityInfoDataWrapper* GetSelectedNodeInfoFromAdapterView(
const mojom::AccessibilityEventData& event_data) const;
// Update android_focused_id_ from given AccessibilityEventData.
// Returns true if it is successfully updated to existing node.
// Returns false if we don't dispatch the processing event to chrome
// automation.
// Updates android_focused_id_ from given AccessibilityEventData.
// Having this method, |android_focused_id_| is one of these:
// - input focus in Android
// - accessibility focus in Android
// - the chrome automation client's internal focus (via set sequential focus
// action and replying accessibility focus event from Android).
// This returns false if we don't want to dispatch the processing
// event to chrome automation. Otherwise, this returns true.
bool UpdateAndroidFocusedId(const mojom::AccessibilityEventData& event_data);
void UpdateAXNameCache(AccessibilityInfoDataWrapper* source_node,
......@@ -173,6 +179,9 @@ class AXTreeSourceArc : public ui::AXTreeSource<AccessibilityInfoDataWrapper*,
std::map<int32_t, std::string> cached_names_;
std::map<int32_t, ax::mojom::Role> cached_roles_;
// Cache of mapping from the root window id to the last focused node id.
std::map<int32_t, int32_t> root_window_id_to_last_focus_node_id_;
// Mapping from Chrome node ID to its cached computed bounds.
// This simplifies bounds calculations.
std::map<int32_t, gfx::Rect> computed_bounds_;
......
......@@ -198,11 +198,9 @@ class AXTreeSourceArcTest : public testing::Test,
EXPECT_EQ(expected, tree_text.substr(first_new_line));
}
void set_screen_reader_mode(bool enabled) {
screen_reader_enabled_ = enabled;
}
void set_full_focus_mode(bool enabled) { full_focus_mode_ = enabled; }
bool IsScreenReaderEnabled() const override { return screen_reader_enabled_; }
bool UseFullFocusMode() const override { return full_focus_mode_; }
private:
void OnAction(const ui::AXActionData& data) const override {}
......@@ -210,7 +208,7 @@ class AXTreeSourceArcTest : public testing::Test,
const std::unique_ptr<MockAutomationEventRouter> router_;
const std::unique_ptr<AXTreeSourceArc> tree_source_;
bool screen_reader_enabled_ = false;
bool full_focus_mode_ = false;
DISALLOW_COPY_AND_ASSIGN(AXTreeSourceArcTest);
};
......@@ -715,9 +713,7 @@ TEST_F(AXTreeSourceArcTest, OnViewSelectedEvent) {
TEST_F(AXTreeSourceArcTest, OnWindowStateChangedEvent) {
auto event = AXEventData::New();
event->source_id = 1; // node1.
event->task_id = 1;
event->event_type = AXEventType::WINDOW_STATE_CHANGED;
event->window_data = std::vector<mojom::AccessibilityWindowInfoDataPtr>();
event->window_data->push_back(AXWindowInfoData::New());
......@@ -735,7 +731,8 @@ TEST_F(AXTreeSourceArcTest, OnWindowStateChangedEvent) {
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node1 = event->node_data.back().get();
node1->id = 1;
SetProperty(node1, AXIntListProperty::CHILD_NODE_IDS, std::vector<int>({2}));
SetProperty(node1, AXIntListProperty::CHILD_NODE_IDS,
std::vector<int>({2, 3}));
SetProperty(node1, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node1, AXBooleanProperty::VISIBLE_TO_USER, true);
......@@ -744,16 +741,63 @@ TEST_F(AXTreeSourceArcTest, OnWindowStateChangedEvent) {
node2->id = 2;
SetProperty(node2, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node2, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(node2, AXStringProperty::TEXT, "sample string.");
SetProperty(node2, AXStringProperty::TEXT, "sample string node2.");
event->node_data.push_back(AXNodeInfoData::New());
AXNodeInfoData* node3 = event->node_data.back().get();
node3->id = 3;
SetProperty(node3, AXBooleanProperty::IMPORTANCE, true);
SetProperty(node3, AXBooleanProperty::VISIBLE_TO_USER, true);
SetProperty(node3, AXStringProperty::TEXT, "sample string node3.");
// Focus will be on the first accessible node (node2).
event->event_type = AXEventType::WINDOW_STATE_CHANGED;
event->source_id = root->id;
CallNotifyAccessibilityEvent(event.get());
ui::AXTreeData data;
// Focus is now at the first accessible node (node2).
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node2->id, data.focus_id);
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
// focus moved to node3 for some reason.
event->event_type = AXEventType::VIEW_FOCUSED;
event->source_id = node3->id;
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node3->id, data.focus_id);
// after moved the focus on the window, keep the same focus on
// WINDOW_STATE_CHANGED event.
event->event_type = AXEventType::WINDOW_STATE_CHANGED;
event->source_id = root->id;
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node3->id, data.focus_id);
// Simulate opening another window in this task.
// This is the same as new WINDOW_STATE_CHANGED event, so focus is at the
// first accessible node (node2).
root_window->window_id = 200;
event->event_type = AXEventType::WINDOW_STATE_CHANGED;
event->source_id = node1->id;
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node2->id, data.focus_id);
// Simulate closing the second window and coming back to the first window.
// The focus back to the last focus node, which is node3.
root_window->window_id = 100;
event->event_type = AXEventType::WINDOW_STATE_CHANGED;
event->source_id = root->id;
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node3->id, data.focus_id);
EXPECT_EQ(5, GetDispatchedEventCount(ax::mojom::Event::kFocus));
}
TEST_F(AXTreeSourceArcTest, OnFocusEvent) {
......@@ -801,14 +845,26 @@ TEST_F(AXTreeSourceArcTest, OnFocusEvent) {
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node2->id, data.focus_id);
// Chrome should focus to node2, even if Android sends focus on List.
// Chrome should focus to node1, even if Android sends focus on List.
event->source_id = root->id;
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node1->id, data.focus_id);
EXPECT_EQ(2, GetDispatchedEventCount(ax::mojom::Event::kFocus));
// VIEW_ACCESSIBILITY_FOCUSED event also updates the focus in screen reader
// mode.
set_full_focus_mode(true);
SetProperty(node1, AXBooleanProperty::ACCESSIBILITY_FOCUSED, false);
SetProperty(node2, AXBooleanProperty::ACCESSIBILITY_FOCUSED, true);
event->event_type = AXEventType::VIEW_ACCESSIBILITY_FOCUSED;
event->source_id = node2->id;
CallNotifyAccessibilityEvent(event.get());
EXPECT_TRUE(CallGetTreeData(&data));
EXPECT_EQ(node2->id, data.focus_id);
EXPECT_EQ(3, GetDispatchedEventCount(ax::mojom::Event::kFocus));
}
TEST_F(AXTreeSourceArcTest, OnDrawerOpened) {
......@@ -912,7 +968,7 @@ TEST_F(AXTreeSourceArcTest, SerializeAndUnserialize) {
// |node2| is ignored by default because
// AXBooleanProperty::IMPORTANCE has a default false value.
set_screen_reader_mode(true);
set_full_focus_mode(true);
CallNotifyAccessibilityEvent(event.get());
EXPECT_EQ(1, GetDispatchedEventCount(ax::mojom::Event::kFocus));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment