[chrome:accessibility] Display a list of accessibility events fired on a page.

Use the accessibility event recorder to capture events fired on a page, and
display the log on chrome:accessibility. This log isn't particularly
meaningful for now, but follow-up CLs will make this be more useful.

Bug: 785493
Change-Id: I5bf88bc070ac7df322cb41847a86642cbf038483
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1823617
Commit-Queue: Abigail Klein <[email protected]>
Reviewed-by: John Abd-El-Malek <[email protected]>
Reviewed-by: Dominic Mazzoni <[email protected]>
Cr-Commit-Position: refs/heads/master@{#711608}
diff --git a/chrome/browser/accessibility/accessibility_ui.cc b/chrome/browser/accessibility/accessibility_ui.cc
index 6bdee07..18b239678 100644
--- a/chrome/browser/accessibility/accessibility_ui.cc
+++ b/chrome/browser/accessibility/accessibility_ui.cc
@@ -54,6 +54,7 @@
 static const char kBrowsersField[] = "browsers";
 static const char kEnabledField[] = "enabled";
 static const char kErrorField[] = "error";
+static const char kEventLogsField[] = "eventLogs";
 static const char kFaviconUrlField[] = "faviconUrl";
 static const char kFlagNameField[] = "flagName";
 static const char kModeIdField[] = "modeId";
@@ -62,9 +63,11 @@
 static const char kPidField[] = "pid";
 static const char kProcessIdField[] = "processId";
 static const char kRequestTypeField[] = "requestType";
+// TODO rename to routingId to match the name elsewhere.
 static const char kRouteIdField[] = "routeId";
 static const char kSessionIdField[] = "sessionId";
 static const char kShouldRequestTreeField[] = "shouldRequestTree";
+static const char kStartField[] = "start";
 static const char kTreeField[] = "tree";
 static const char kTypeField[] = "type";
 static const char kUrlField[] = "url";
@@ -372,6 +375,11 @@
       "requestNativeUITree",
       base::BindRepeating(&AccessibilityUIMessageHandler::RequestNativeUITree,
                           base::Unretained(this)));
+  web_ui()->RegisterMessageCallback(
+      "requestAccessibilityEvents",
+      base::BindRepeating(
+          &AccessibilityUIMessageHandler::RequestAccessibilityEvents,
+          base::Unretained(this)));
 }
 
 void AccessibilityUIMessageHandler::ToggleAccessibility(
@@ -613,6 +621,52 @@
   CallJavascriptFunction(request_type, *(result.get()));
 }
 
+void AccessibilityUIMessageHandler::Callback(const std::string& str) {
+  event_logs_.push_back(str);
+}
+
+void AccessibilityUIMessageHandler::RequestAccessibilityEvents(
+    const base::ListValue* args) {
+  const base::DictionaryValue* data;
+  CHECK(args->GetDictionary(0, &data));
+
+  int process_id = *data->FindIntPath(kProcessIdField);
+  int route_id = *data->FindIntPath(kRouteIdField);
+  bool start = *data->FindBoolPath(kStartField);
+
+  AllowJavascript();
+
+  content::RenderViewHost* rvh =
+      content::RenderViewHost::FromID(process_id, route_id);
+  if (!rvh) {
+    return;
+  }
+
+  std::unique_ptr<base::DictionaryValue> result(BuildTargetDescriptor(rvh));
+  content::WebContents* web_contents =
+      content::WebContents::FromRenderViewHost(rvh);
+  if (start) {
+    web_contents->RecordAccessibilityEvents(
+        base::BindRepeating(&AccessibilityUIMessageHandler::Callback,
+                            base::Unretained(this)),
+        true);
+  } else {
+    web_contents->RecordAccessibilityEvents(
+        base::BindRepeating(&AccessibilityUIMessageHandler::Callback,
+                            base::Unretained(this)),
+        false);
+    std::string event_logs_str;
+    for (std::string log : event_logs_) {
+      event_logs_str += log;
+      event_logs_str += "\n";
+    }
+    result->SetString(kEventLogsField, event_logs_str);
+    event_logs_.clear();
+
+    CallJavascriptFunction("accessibility.startOrStopEvents", *(result.get()));
+  }
+}
+
 // static
 void AccessibilityUIMessageHandler::RegisterProfilePrefs(
     user_prefs::PrefRegistrySyncable* registry) {
diff --git a/chrome/browser/accessibility/accessibility_ui.h b/chrome/browser/accessibility/accessibility_ui.h
index 8a2f32e..545dffe6 100644
--- a/chrome/browser/accessibility/accessibility_ui.h
+++ b/chrome/browser/accessibility/accessibility_ui.h
@@ -34,10 +34,14 @@
   static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
 
  private:
+  std::vector<std::string> event_logs_;
+
   void ToggleAccessibility(const base::ListValue* args);
   void SetGlobalFlag(const base::ListValue* args);
   void RequestWebContentsTree(const base::ListValue* args);
   void RequestNativeUITree(const base::ListValue* args);
+  void RequestAccessibilityEvents(const base::ListValue* args);
+  void Callback(const std::string&);
 
   DISALLOW_COPY_AND_ASSIGN(AccessibilityUIMessageHandler);
 };
diff --git a/chrome/browser/resources/accessibility/accessibility.js b/chrome/browser/resources/accessibility/accessibility.js
index 7db0aaf..31a6377 100644
--- a/chrome/browser/resources/accessibility/accessibility.js
+++ b/chrome/browser/resources/accessibility/accessibility.js
@@ -100,6 +100,25 @@
     }
   }
 
+  function requestEvents(data, element) {
+    const start = element.textContent == 'Start recording';
+    if (start) {
+      element.textContent = 'Stop recording';
+      element.setAttribute('aria-expanded', 'true');
+
+      // TODO Hide all other start recording elements. UI should reflect the
+      // fact that there can only be one accessibility recorder at once.
+    } else {
+      element.textContent = 'Start recording';
+      element.setAttribute('aria-expanded', 'false');
+
+      // TODO Show all start recording elements.
+    }
+    chrome.send('requestAccessibilityEvents', [
+      {'processId': data.processId, 'routeId': data.routeId, 'start': start}
+    ]);
+  }
+
   function initialize() {
     console.log('initialize');
     const data = requestData();
@@ -178,7 +197,7 @@
   function formatRow(row, data) {
     if (!('url' in data)) {
       if ('error' in data) {
-        row.appendChild(createErrorMessageElement(data, row));
+        row.appendChild(createErrorMessageElement(data));
         return;
       }
     }
@@ -206,14 +225,24 @@
 
     row.appendChild(document.createTextNode(' | '));
 
-    if ('tree' in data) {
-      row.appendChild(createTreeButtons(data, row.id));
-    } else {
-      row.appendChild(createShowAccessibilityTreeElement(data, row.id, false));
+    const hasTree = 'tree' in data;
+    row.appendChild(createShowAccessibilityTreeElement(data, row.id, hasTree));
+    if (navigator.clipboard) {
       row.appendChild(createCopyAccessibilityTreeElement(data, row.id));
-      if ('error' in data) {
-        row.appendChild(createErrorMessageElement(data, row));
-      }
+    }
+    if (hasTree) {
+      row.appendChild(createHideAccessibilityTreeElement(row.id));
+    }
+    row.appendChild(
+        createStartStopAccessibilityEventRecordingElement(data, row.id));
+
+    if (hasTree) {
+      row.appendChild(createAccessibilityOutputElement(data, row.id, 'tree'));
+    } else if ('eventLogs' in data) {
+      row.appendChild(
+          createAccessibilityOutputElement(data, row.id, 'eventLogs'));
+    } else if ('error' in data) {
+      row.appendChild(createErrorMessageElement(data));
     }
   }
 
@@ -292,17 +321,6 @@
     return link;
   }
 
-  function createTreeButtons(data, id) {
-    const row = document.createElement('span');
-    row.appendChild(createShowAccessibilityTreeElement(data, id, true));
-    if (navigator.clipboard) {
-      row.appendChild(createCopyAccessibilityTreeElement(data, id));
-    }
-    row.appendChild(createHideAccessibilityTreeElement(id));
-    row.appendChild(createAccessibilityTreeElement(data, id));
-    return row;
-  }
-
   function createShowAccessibilityTreeElement(data, id, opt_refresh) {
     const show = document.createElement('button');
     if (opt_refresh) {
@@ -344,6 +362,15 @@
     return copy;
   }
 
+  function createStartStopAccessibilityEventRecordingElement(data, id) {
+    const show = document.createElement('button');
+    show.textContent = 'Start recording';
+    show.id = id + ':startOrStopEvents';
+    show.setAttribute('aria-expanded', 'false');
+    show.addEventListener('click', requestEvents.bind(this, data, show));
+    return show;
+  }
+
   function createErrorMessageElement(data) {
     const errorMessageElement = document.createElement('div');
     const errorMessage = data.error;
@@ -376,6 +403,19 @@
   }
 
   // Called from C++
+  function startOrStopEvents(data) {
+    const id = getIdFromData(data);
+    const row = $(id);
+    if (!row) {
+      return;
+    }
+
+    row.textContent = '';
+    formatRow(row, data);
+    $(id + ':startOrStopEvents').focus();
+  }
+
+  // Called from C++
   function copyTree(data) {
     const id = getIdFromData(data);
     const row = $(id);
@@ -416,15 +456,14 @@
     return row;
   }
 
-  function createAccessibilityTreeElement(data, id) {
-    let treeElement = $(id + ':tree');
-    if (treeElement) {
-      treeElement.style.display = '';
-    } else {
+  // type is either 'tree' or 'eventLogs'
+  function createAccessibilityOutputElement(data, id, type) {
+    let treeElement = $(id + ':' + type);
+    if (!treeElement) {
       treeElement = document.createElement('pre');
-      treeElement.id = id + ':tree';
+      treeElement.id = id + ':' + type;
     }
-    treeElement.textContent = data.tree;
+    treeElement.textContent = data[type];
     return treeElement;
   }
 
@@ -432,7 +471,8 @@
   return {
     copyTree: copyTree,
     initialize: initialize,
-    showOrRefreshTree: showOrRefreshTree
+    showOrRefreshTree: showOrRefreshTree,
+    startOrStopEvents: startOrStopEvents
   };
 });
 
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index 5d1e281..dcd4853 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -306,6 +306,12 @@
     "../common/service_manager/child_connection.h",
     "about_url_loader_factory.cc",
     "about_url_loader_factory.h",
+    "accessibility/accessibility_event_recorder.cc",
+    "accessibility/accessibility_event_recorder.h",
+    "accessibility/accessibility_event_recorder_mac.mm",
+    "accessibility/accessibility_event_recorder_uia_win.cc",
+    "accessibility/accessibility_event_recorder_uia_win.h",
+    "accessibility/accessibility_event_recorder_win.cc",
     "accessibility/accessibility_tree_formatter_base.cc",
     "accessibility/accessibility_tree_formatter_base.h",
     "accessibility/accessibility_tree_formatter_blink.cc",
@@ -2279,6 +2285,7 @@
 
   if (use_atk) {
     sources += [
+      "accessibility/accessibility_event_recorder_auralinux.cc",
       "accessibility/accessibility_tree_formatter_auralinux.cc",
       "accessibility/accessibility_tree_formatter_utils_auralinux.cc",
       "accessibility/accessibility_tree_formatter_utils_auralinux.h",
diff --git a/content/browser/accessibility/accessibility_event_recorder.h b/content/browser/accessibility/accessibility_event_recorder.h
index d9ee28c..6a9adf7 100644
--- a/content/browser/accessibility/accessibility_event_recorder.h
+++ b/content/browser/accessibility/accessibility_event_recorder.h
@@ -12,6 +12,7 @@
 #include "base/callback.h"
 #include "base/macros.h"
 #include "base/process/process_handle.h"
+#include "content/common/content_export.h"
 
 namespace content {
 
@@ -35,7 +36,7 @@
 // each platform does most of the work.
 //
 // As currently designed, there should only be one instance of this class.
-class AccessibilityEventRecorder {
+class CONTENT_EXPORT AccessibilityEventRecorder {
  public:
   // Construct the right platform-specific subclass.
   static std::unique_ptr<AccessibilityEventRecorder> Create(
diff --git a/content/browser/accessibility/accessibility_event_recorder_auralinux.cc b/content/browser/accessibility/accessibility_event_recorder_auralinux.cc
index 76678e7..bbb3320 100644
--- a/content/browser/accessibility/accessibility_event_recorder_auralinux.cc
+++ b/content/browser/accessibility/accessibility_event_recorder_auralinux.cc
@@ -63,7 +63,6 @@
   AtspiEventListener* atspi_event_listener_ = nullptr;
   base::ProcessId pid_;
   base::StringPiece application_name_match_pattern_;
-  static std::vector<unsigned int> atk_listener_ids_;
   static AccessibilityEventRecorderAuraLinux* instance_;
 
   DISALLOW_COPY_AND_ASSIGN(AccessibilityEventRecorderAuraLinux);
@@ -72,9 +71,12 @@
 // static
 AccessibilityEventRecorderAuraLinux*
     AccessibilityEventRecorderAuraLinux::instance_ = nullptr;
-std::vector<unsigned int>
-    content::AccessibilityEventRecorderAuraLinux::atk_listener_ids_ =
-        std::vector<unsigned int>();
+
+// static
+std::vector<unsigned int>& GetATKListenerIds() {
+  static base::NoDestructor<std::vector<unsigned int>> atk_listener_ids;
+  return *atk_listener_ids;
+}
 
 // static
 gboolean AccessibilityEventRecorderAuraLinux::OnATKEventReceived(
@@ -145,11 +147,12 @@
   if (!id)
     LOG(FATAL) << "atk_add_global_event_listener failed for " << event_name;
 
-  atk_listener_ids_.push_back(id);
+  std::vector<unsigned int>& atk_listener_ids = GetATKListenerIds();
+  atk_listener_ids.push_back(id);
 }
 
 void AccessibilityEventRecorderAuraLinux::AddATKEventListeners() {
-  if (atk_listener_ids_.size() >= 1)
+  if (GetATKListenerIds().size() >= 1)
     return;
   GObject* gobject = G_OBJECT(g_object_new(G_TYPE_OBJECT, nullptr, nullptr));
   g_object_unref(atk_no_op_object_new(gobject));
@@ -165,10 +168,11 @@
 }
 
 void AccessibilityEventRecorderAuraLinux::RemoveATKEventListeners() {
-  for (const auto& id : atk_listener_ids_)
+  std::vector<unsigned int>& atk_listener_ids = GetATKListenerIds();
+  for (const auto& id : atk_listener_ids)
     atk_remove_global_event_listener(id);
 
-  atk_listener_ids_.clear();
+  atk_listener_ids.clear();
 }
 
 // Pruning states which are not supported on older bots makes it possible to
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index dafbbbc..c960b2b 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -41,6 +41,7 @@
 #include "components/download/public/common/download_stats.h"
 #include "components/rappor/public/rappor_utils.h"
 #include "components/url_formatter/url_formatter.h"
+#include "content/browser/accessibility/accessibility_event_recorder.h"
 #include "content/browser/accessibility/accessibility_tree_formatter_blink.h"
 #include "content/browser/bad_message.h"
 #include "content/browser/browser_main_loop.h"
@@ -186,6 +187,10 @@
 #endif
 
 namespace content {
+
+using AccessibilityEventCallback =
+    base::RepeatingCallback<void(const std::string&)>;
+
 namespace {
 
 const int kMinimumDelayBetweenLoadingUpdatesMS = 100;
@@ -3254,6 +3259,24 @@
       ax_mgr, internal, property_filters);
 }
 
+void WebContentsImpl::RecordAccessibilityEvents(
+    AccessibilityEventCallback callback,
+    bool start) {
+  if (start) {
+    SetAccessibilityMode(ui::AXMode::kWebContents);
+    auto* ax_mgr = GetOrCreateRootBrowserAccessibilityManager();
+    DCHECK(ax_mgr);
+    base::ProcessId pid = base::Process::Current().Pid();
+    event_recorder_ = content::AccessibilityEventRecorder::Create(
+        ax_mgr, pid, base::StringPiece{});
+    event_recorder_->ListenToEvents(callback);
+  } else {
+    DCHECK(event_recorder_);
+    event_recorder_->FlushAsyncEvents();
+    event_recorder_ = nullptr;
+  }
+}
+
 RenderFrameHost* WebContentsImpl::GetGuestByInstanceID(
     RenderFrameHost* render_frame_host,
     int browser_plugin_instance_id) {
diff --git a/content/browser/web_contents/web_contents_impl.h b/content/browser/web_contents/web_contents_impl.h
index 40f1bb1..8b8caa8 100644
--- a/content/browser/web_contents/web_contents_impl.h
+++ b/content/browser/web_contents/web_contents_impl.h
@@ -26,6 +26,7 @@
 #include "base/values.h"
 #include "build/build_config.h"
 #include "components/download/public/common/download_url_parameters.h"
+#include "content/browser/accessibility/accessibility_event_recorder.h"
 #include "content/browser/frame_host/frame_tree.h"
 #include "content/browser/frame_host/frame_tree_node.h"
 #include "content/browser/frame_host/interstitial_page_impl.h"
@@ -135,6 +136,9 @@
 class PepperPlaybackObserver;
 #endif
 
+using AccessibilityEventCallback =
+    base::RepeatingCallback<void(const std::string&)>;
+
 // Factory function for the implementations that content knows about. Takes
 // ownership of |delegate|.
 WebContentsView* CreateWebContentsView(
@@ -564,6 +568,8 @@
       bool internal,
       std::vector<content::AccessibilityTreeFormatter::PropertyFilter>
           property_filters) override;
+  void RecordAccessibilityEvents(AccessibilityEventCallback callback,
+                                 bool start) override;
   RenderFrameHost* GetGuestByInstanceID(
       RenderFrameHost* render_frame_host,
       int browser_plugin_instance_id) override;
@@ -1792,6 +1798,8 @@
   // is created, and broadcast to all frames when it changes.
   ui::AXMode accessibility_mode_;
 
+  std::unique_ptr<content::AccessibilityEventRecorder> event_recorder_;
+
   // Monitors power levels for audio streams associated with this WebContents.
   AudioStreamMonitor audio_stream_monitor_;
 
diff --git a/content/public/browser/web_contents.h b/content/public/browser/web_contents.h
index 075dd254..3b6ac0f 100644
--- a/content/public/browser/web_contents.h
+++ b/content/public/browser/web_contents.h
@@ -411,6 +411,18 @@
       std::vector<content::AccessibilityTreeFormatter::PropertyFilter>
           property_filters) = 0;
 
+  // A callback that takes a string which contains accessibility event
+  // information.
+  using AccessibilityEventCallback =
+      base::RepeatingCallback<void(const std::string&)>;
+
+  // Starts or stops recording accessibility events. While accessibility events
+  // are being recorded, the callback will be called when an accessibility
+  // event is received. The start paramater says whether the recording is
+  // starting or stopping.
+  virtual void RecordAccessibilityEvents(AccessibilityEventCallback callback,
+                                         bool start) = 0;
+
   virtual const PageImportanceSignals& GetPageImportanceSignals() = 0;
 
   // Tab navigation state ------------------------------------------------------
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 631d5697..9cf599b9 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -37,12 +37,6 @@
   }
 
   sources = [
-    "../browser/accessibility/accessibility_event_recorder.cc",
-    "../browser/accessibility/accessibility_event_recorder.h",
-    "../browser/accessibility/accessibility_event_recorder_mac.mm",
-    "../browser/accessibility/accessibility_event_recorder_uia_win.cc",
-    "../browser/accessibility/accessibility_event_recorder_uia_win.h",
-    "../browser/accessibility/accessibility_event_recorder_win.cc",
     "../browser/accessibility/test_browser_accessibility_delegate.cc",
     "../browser/accessibility/test_browser_accessibility_delegate.h",
     "../browser/background_fetch/background_fetch_test_base.cc",
@@ -500,8 +494,6 @@
   }
 
   if (use_atk) {
-    sources +=
-        [ "../browser/accessibility/accessibility_event_recorder_auralinux.cc" ]
     configs += [
       "//build/config/linux/atk",
       "//build/config/linux/atspi2",