| // Copyright 2018 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 "content/browser/accessibility/accessibility_event_recorder.h" |
| |
| #include <atk/atk.h> |
| #include <atk/atkutil.h> |
| #include <atspi/atspi.h> |
| |
| #include "base/process/process_handle.h" |
| #include "base/stl_util.h" |
| #include "base/strings/pattern.h" |
| #include "base/strings/stringprintf.h" |
| #include "content/browser/accessibility/accessibility_tree_formatter_utils_auralinux.h" |
| #include "content/browser/accessibility/browser_accessibility_auralinux.h" |
| #include "content/browser/accessibility/browser_accessibility_manager.h" |
| |
| #if defined(ATK_CHECK_VERSION) && ATK_CHECK_VERSION(2, 16, 0) |
| #define ATK_216 |
| #endif |
| |
| namespace content { |
| |
| // This class has two distinct event recording code paths. When we are |
| // recording events in-process (typically this is used for |
| // DumpAccessibilityEvents tests), we use ATK's global event handlers. Since |
| // ATK doesn't support intercepting events from other processes, if we have a |
| // non-zero PID or an accessibility application name pattern, we use AT-SPI2 |
| // directly to intercept events. Since AT-SPI2 should be capable of |
| // intercepting events in-process as well, eventually it would be nice to |
| // remove the ATK code path entirely. |
| class AccessibilityEventRecorderAuraLinux : public AccessibilityEventRecorder { |
| public: |
| explicit AccessibilityEventRecorderAuraLinux( |
| BrowserAccessibilityManager* manager, |
| base::ProcessId pid, |
| const base::StringPiece& application_name_match_pattern); |
| ~AccessibilityEventRecorderAuraLinux() override; |
| |
| void ProcessATKEvent(const char* event, |
| unsigned int n_params, |
| const GValue* params); |
| void ProcessATSPIEvent(const AtspiEvent* event); |
| |
| static gboolean OnATKEventReceived(GSignalInvocationHint* hint, |
| unsigned int n_params, |
| const GValue* params, |
| gpointer data); |
| |
| private: |
| bool ShouldUseATSPI(); |
| |
| void AddATKEventListener(const char* event_name); |
| void AddATKEventListeners(); |
| void RemoveATKEventListeners(); |
| bool IncludeState(AtkStateType state_type); |
| |
| void AddATSPIEventListeners(); |
| void RemoveATSPIEventListeners(); |
| |
| AtspiEventListener* atspi_event_listener_ = nullptr; |
| base::ProcessId pid_; |
| base::StringPiece application_name_match_pattern_; |
| std::vector<unsigned int> atk_listener_ids_; |
| static AccessibilityEventRecorderAuraLinux* instance_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AccessibilityEventRecorderAuraLinux); |
| }; |
| |
| // static |
| AccessibilityEventRecorderAuraLinux* |
| AccessibilityEventRecorderAuraLinux::instance_ = nullptr; |
| |
| // static |
| gboolean AccessibilityEventRecorderAuraLinux::OnATKEventReceived( |
| GSignalInvocationHint* hint, |
| unsigned int n_params, |
| const GValue* params, |
| gpointer data) { |
| GSignalQuery query; |
| g_signal_query(hint->signal_id, &query); |
| |
| if (instance_) { |
| instance_->ProcessATKEvent(query.signal_name, n_params, params); |
| } |
| |
| return true; |
| } |
| |
| // static |
| std::unique_ptr<AccessibilityEventRecorder> AccessibilityEventRecorder::Create( |
| BrowserAccessibilityManager* manager, |
| base::ProcessId pid, |
| const base::StringPiece& application_name_match_pattern) { |
| return std::make_unique<AccessibilityEventRecorderAuraLinux>( |
| manager, pid, application_name_match_pattern); |
| } |
| |
| std::vector<AccessibilityEventRecorder::EventRecorderFactory> |
| AccessibilityEventRecorder::GetTestPasses() { |
| // Both the Blink pass and native pass use the same recorder |
| return { |
| &AccessibilityEventRecorder::Create, |
| &AccessibilityEventRecorder::Create, |
| }; |
| } |
| |
| bool AccessibilityEventRecorderAuraLinux::ShouldUseATSPI() { |
| return pid_ != base::GetCurrentProcId() || |
| !application_name_match_pattern_.empty(); |
| } |
| |
| AccessibilityEventRecorderAuraLinux::AccessibilityEventRecorderAuraLinux( |
| BrowserAccessibilityManager* manager, |
| base::ProcessId pid, |
| const base::StringPiece& application_name_match_pattern) |
| : AccessibilityEventRecorder(manager), |
| pid_(pid), |
| application_name_match_pattern_(application_name_match_pattern) { |
| CHECK(!instance_) << "There can be only one instance of" |
| << " AccessibilityEventRecorder at a time."; |
| |
| if (ShouldUseATSPI()) { |
| AddATSPIEventListeners(); |
| } else { |
| AddATKEventListeners(); |
| } |
| |
| instance_ = this; |
| } |
| |
| AccessibilityEventRecorderAuraLinux::~AccessibilityEventRecorderAuraLinux() { |
| RemoveATSPIEventListeners(); |
| instance_ = nullptr; |
| } |
| |
| void AccessibilityEventRecorderAuraLinux::AddATKEventListener( |
| const char* event_name) { |
| unsigned id = atk_add_global_event_listener(OnATKEventReceived, event_name); |
| if (!id) |
| LOG(FATAL) << "atk_add_global_event_listener failed for " << event_name; |
| |
| atk_listener_ids_.push_back(id); |
| } |
| |
| void AccessibilityEventRecorderAuraLinux::AddATKEventListeners() { |
| GObject* gobject = G_OBJECT(g_object_new(G_TYPE_OBJECT, nullptr, nullptr)); |
| g_object_unref(atk_no_op_object_new(gobject)); |
| g_object_unref(gobject); |
| |
| AddATKEventListener("ATK:AtkObject:state-change"); |
| AddATKEventListener("ATK:AtkObject:focus-event"); |
| AddATKEventListener("ATK:AtkObject:property-change"); |
| AddATKEventListener("ATK:AtkSelection:selection-changed"); |
| } |
| |
| void AccessibilityEventRecorderAuraLinux::RemoveATKEventListeners() { |
| for (const auto& id : atk_listener_ids_) |
| atk_remove_global_event_listener(id); |
| |
| atk_listener_ids_.clear(); |
| } |
| |
| // Pruning states which are not supported on older bots makes it possible to |
| // run the events tests in more environments. |
| bool AccessibilityEventRecorderAuraLinux::IncludeState( |
| AtkStateType state_type) { |
| switch (state_type) { |
| #if defined(ATK_216) |
| case ATK_STATE_CHECKABLE: |
| case ATK_STATE_HAS_POPUP: |
| case ATK_STATE_READ_ONLY: |
| return false; |
| #endif |
| case ATK_STATE_LAST_DEFINED: |
| return false; |
| default: |
| return true; |
| } |
| } |
| |
| void AccessibilityEventRecorderAuraLinux::ProcessATKEvent( |
| const char* event, |
| unsigned int n_params, |
| const GValue* params) { |
| // If we don't have a root object, it means the tree is being destroyed. |
| if (!manager_->GetRoot()) { |
| RemoveATKEventListeners(); |
| return; |
| } |
| |
| std::string log; |
| if (std::string(event).find("property-change") != std::string::npos) { |
| DCHECK_GE(n_params, 2u); |
| AtkPropertyValues* property_values = |
| static_cast<AtkPropertyValues*>(g_value_get_pointer(¶ms[1])); |
| |
| if (g_strcmp0(property_values->property_name, "accessible-value") == 0) { |
| log += "VALUE-CHANGED:"; |
| log += |
| base::NumberToString(g_value_get_double(&property_values->new_value)); |
| } else if (g_strcmp0(property_values->property_name, "accessible-name") == |
| 0) { |
| log += "NAME-CHANGED:"; |
| log += g_value_get_string(&property_values->new_value); |
| } else if (g_strcmp0(property_values->property_name, |
| "accessible-description") == 0) { |
| log += "DESCRIPTION-CHANGED:"; |
| log += g_value_get_string(&property_values->new_value); |
| } else { |
| return; |
| } |
| } else { |
| log += base::ToUpperASCII(event); |
| if (std::string(event).find("state-change") != std::string::npos) { |
| log += ":" + base::ToUpperASCII(g_value_get_string(¶ms[1])); |
| log += base::StringPrintf(":%s", g_strdup_value_contents(¶ms[2])); |
| } |
| } |
| |
| AtkObject* obj = ATK_OBJECT(g_value_get_object(¶ms[0])); |
| std::string role = atk_role_get_name(atk_object_get_role(obj)); |
| base::ReplaceChars(role, " ", "_", &role); |
| log += base::StringPrintf(" role=ROLE_%s", base::ToUpperASCII(role).c_str()); |
| log += base::StringPrintf(" name='%s'", atk_object_get_name(obj)); |
| |
| std::string states = ""; |
| AtkStateSet* state_set = atk_object_ref_state_set(obj); |
| for (int i = ATK_STATE_INVALID; i < ATK_STATE_LAST_DEFINED; i++) { |
| AtkStateType state_type = static_cast<AtkStateType>(i); |
| if (atk_state_set_contains_state(state_set, state_type) && |
| IncludeState(state_type)) { |
| states += " " + base::ToUpperASCII(atk_state_type_get_name(state_type)); |
| } |
| } |
| states = base::CollapseWhitespaceASCII(states, false); |
| base::ReplaceChars(states, " ", ",", &states); |
| log += base::StringPrintf(" %s", states.c_str()); |
| |
| OnEvent(log); |
| } |
| |
| // This list is composed of the sorted event names taken from the list provided |
| // in the libatspi documentation at: |
| // https://developer.gnome.org/libatspi/stable/AtspiEventListener.html#atspi-event-listener-register |
| const char* const kEventNames[] = { |
| "object:active-descendant-changed", |
| "object:children-changed", |
| "object:column-deleted", |
| "object:column-inserted", |
| "object:column-reordered", |
| "object:model-changed", |
| "object:property-change", |
| "object:property-change:accessible-description", |
| "object:property-change:accessible-name", |
| "object:property-change:accessible-parent", |
| "object:property-change:accessible-role", |
| "object:property-change:accessible-table-caption", |
| "object:property-change:accessible-table-column-description", |
| "object:property-change:accessible-table-column-header", |
| "object:property-change:accessible-table-row-description", |
| "object:property-change:accessible-table-row-header", |
| "object:property-change:accessible-table-summary", |
| "object:property-change:accessible-value", |
| "object:row-deleted", |
| "object:row-inserted", |
| "object:row-reordered", |
| "object:selection-changed", |
| "object:state-changed", |
| "object:text-caret-moved", |
| "object:text-changed", |
| "object:text-selection-changed", |
| "object:visible-data-changed", |
| "window:activate", |
| "window:close", |
| "window:create", |
| "window:deactivate", |
| "window:desktop-create", |
| "window:desktop-destroy", |
| "window:lower", |
| "window:maximize", |
| "window:minimize", |
| "window:move", |
| "window:raise", |
| "window:reparent", |
| "window:resize", |
| "window:restore", |
| "window:restyle", |
| "window:shade", |
| "window:unshade", |
| }; |
| |
| static void OnATSPIEventReceived(AtspiEvent* event, void* data) { |
| static_cast<AccessibilityEventRecorderAuraLinux*>(data)->ProcessATSPIEvent( |
| event); |
| g_boxed_free(ATSPI_TYPE_EVENT, static_cast<void*>(event)); |
| } |
| |
| void AccessibilityEventRecorderAuraLinux::AddATSPIEventListeners() { |
| atspi_init(); |
| atspi_event_listener_ = |
| atspi_event_listener_new(OnATSPIEventReceived, this, nullptr); |
| |
| GError* error = nullptr; |
| for (size_t i = 0; i < base::size(kEventNames); i++) { |
| atspi_event_listener_register(atspi_event_listener_, kEventNames[i], |
| &error); |
| if (error) { |
| LOG(ERROR) << "Could not register event listener for " << kEventNames[i]; |
| g_clear_error(&error); |
| } |
| } |
| } |
| |
| void AccessibilityEventRecorderAuraLinux::RemoveATSPIEventListeners() { |
| if (!atspi_event_listener_) |
| return; |
| |
| GError* error = nullptr; |
| for (size_t i = 0; i < base::size(kEventNames); i++) { |
| atspi_event_listener_deregister(atspi_event_listener_, kEventNames[i], |
| nullptr); |
| if (error) { |
| LOG(ERROR) << "Could not deregister event listener for " |
| << kEventNames[i]; |
| g_clear_error(&error); |
| } |
| } |
| |
| g_object_unref(atspi_event_listener_); |
| atspi_event_listener_ = nullptr; |
| } |
| |
| void AccessibilityEventRecorderAuraLinux::ProcessATSPIEvent( |
| const AtspiEvent* event) { |
| GError* error = nullptr; |
| |
| if (!application_name_match_pattern_.empty()) { |
| AtspiAccessible* application = |
| atspi_accessible_get_application(event->source, &error); |
| if (error || !application) |
| return; |
| |
| char* application_name = atspi_accessible_get_name(application, &error); |
| g_object_unref(application); |
| if (error || !application_name) { |
| g_clear_error(&error); |
| return; |
| } |
| |
| if (!base::MatchPattern(application_name, |
| application_name_match_pattern_)) { |
| return; |
| } |
| free(application_name); |
| } |
| |
| if (pid_) { |
| int pid = atspi_accessible_get_process_id(event->source, &error); |
| if (!error && pid != pid_) |
| return; |
| g_clear_error(&error); |
| } |
| |
| std::stringstream output; |
| output << event->type << " "; |
| |
| GHashTable* attributes = |
| atspi_accessible_get_attributes(event->source, &error); |
| std::string html_tag, html_class, html_id; |
| if (!error && attributes) { |
| if (char* tag = static_cast<char*>(g_hash_table_lookup(attributes, "tag"))) |
| html_tag = tag; |
| if (char* id = static_cast<char*>(g_hash_table_lookup(attributes, "id"))) |
| html_id = id; |
| if (char* class_chars = |
| static_cast<char*>(g_hash_table_lookup(attributes, "class"))) |
| html_class = std::string(".") + class_chars; |
| g_hash_table_unref(attributes); |
| } |
| g_clear_error(&error); |
| |
| if (!html_tag.empty()) |
| output << "<" << html_tag << html_id << html_class << ">"; |
| |
| AtspiRole role = atspi_accessible_get_role(event->source, &error); |
| output << "role="; |
| if (!error) |
| output << ATSPIRoleToString(role); |
| else |
| output << "#error"; |
| g_clear_error(&error); |
| |
| char* name = atspi_accessible_get_name(event->source, &error); |
| output << " name="; |
| if (!error && name) |
| output << name; |
| else |
| output << "#error"; |
| g_clear_error(&error); |
| free(name); |
| |
| AtspiStateSet* atspi_states = atspi_accessible_get_state_set(event->source); |
| GArray* state_array = atspi_state_set_get_states(atspi_states); |
| std::vector<std::string> states; |
| for (unsigned i = 0; i < state_array->len; i++) { |
| AtspiStateType state_type = g_array_index(state_array, AtspiStateType, i); |
| states.push_back(ATSPIStateToString(state_type)); |
| } |
| g_array_free(state_array, TRUE); |
| g_object_unref(atspi_states); |
| output << " "; |
| std::copy(states.begin(), states.end(), |
| std::ostream_iterator<std::string>(output, ", ")); |
| |
| OnEvent(output.str()); |
| } |
| |
| } // namespace content |