Re-subimission of https://codereview.chromium.org/1041213003/
[chromium-blink-merge.git] / content / browser / accessibility / accessibility_event_recorder_win.cc
blob6fa8aae374c63abfbb988c4188ff18ee7589a349
1 // Copyright 2014 The Chromium 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.
5 #include "content/browser/accessibility/accessibility_event_recorder.h"
7 #include <oleacc.h>
9 #include <string>
11 #include "base/strings/string_number_conversions.h"
12 #include "base/strings/string_util.h"
13 #include "base/strings/stringprintf.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "base/win/scoped_bstr.h"
16 #include "base/win/scoped_comptr.h"
17 #include "base/win/scoped_variant.h"
18 #include "content/browser/accessibility/accessibility_tree_formatter_utils_win.h"
19 #include "content/browser/accessibility/browser_accessibility_manager.h"
20 #include "content/browser/accessibility/browser_accessibility_win.h"
21 #include "third_party/iaccessible2/ia2_api_all.h"
22 #include "ui/base/win/atl_module.h"
24 namespace content {
26 namespace {
28 std::string RoleVariantToString(const base::win::ScopedVariant& role) {
29 if (role.type() == VT_I4) {
30 return base::UTF16ToUTF8(IAccessibleRoleToString(V_I4(role.ptr())));
31 } else if (role.type() == VT_BSTR) {
32 return base::UTF16ToUTF8(
33 base::string16(V_BSTR(role.ptr()), SysStringLen(V_BSTR(role.ptr()))));
35 return std::string();
38 HRESULT QueryIAccessible2(IAccessible* accessible, IAccessible2** accessible2) {
39 base::win::ScopedComPtr<IServiceProvider> service_provider;
40 HRESULT hr = accessible->QueryInterface(service_provider.Receive());
41 return SUCCEEDED(hr) ?
42 service_provider->QueryService(IID_IAccessible2, accessible2) : hr;
45 HRESULT QueryIAccessibleText(IAccessible* accessible,
46 IAccessibleText** accessible_text) {
47 base::win::ScopedComPtr<IServiceProvider> service_provider;
48 HRESULT hr = accessible->QueryInterface(service_provider.Receive());
49 return SUCCEEDED(hr) ?
50 service_provider->QueryService(IID_IAccessibleText, accessible_text) : hr;
53 std::string BstrToUTF8(BSTR bstr) {
54 base::string16 str16(bstr, SysStringLen(bstr));
56 // IAccessibleText returns the text you get by appending all static text
57 // children, with an "embedded object character" for each non-text child.
58 // Pretty-print the embedded object character as <obj> so that test output
59 // is human-readable.
60 base::ReplaceChars(str16, L"\xfffc", L"<obj>", &str16);
62 return base::UTF16ToUTF8(str16);
65 std::string AccessibilityEventToStringUTF8(int32 event_id) {
66 return base::UTF16ToUTF8(AccessibilityEventToString(event_id));
69 } // namespace
71 class AccessibilityEventRecorderWin : public AccessibilityEventRecorder {
72 public:
73 explicit AccessibilityEventRecorderWin(BrowserAccessibilityManager* manager);
74 virtual ~AccessibilityEventRecorderWin();
76 // Callback registered by SetWinEventHook. Just calls OnWinEventHook.
77 static void CALLBACK WinEventHookThunk(
78 HWINEVENTHOOK handle,
79 DWORD event,
80 HWND hwnd,
81 LONG obj_id,
82 LONG child_id,
83 DWORD event_thread,
84 DWORD event_time);
86 private:
87 // Called by the thunk registered by SetWinEventHook. Retrives accessibility
88 // info about the node the event was fired on and appends a string to
89 // the event log.
90 void OnWinEventHook(HWINEVENTHOOK handle,
91 DWORD event,
92 HWND hwnd,
93 LONG obj_id,
94 LONG child_id,
95 DWORD event_thread,
96 DWORD event_time);
98 // Wrapper around AccessibleObjectFromWindow because the function call
99 // inexplicably flakes sometimes on build/trybots.
100 HRESULT AccessibleObjectFromWindowWrapper(
101 HWND hwnd, DWORD dwId, REFIID riid, void **ppvObject);
103 HWINEVENTHOOK win_event_hook_handle_;
104 static AccessibilityEventRecorderWin* instance_;
107 // static
108 AccessibilityEventRecorderWin*
109 AccessibilityEventRecorderWin::instance_ = nullptr;
111 // static
112 AccessibilityEventRecorder* AccessibilityEventRecorder::Create(
113 BrowserAccessibilityManager* manager) {
114 return new AccessibilityEventRecorderWin(manager);
117 // static
118 void CALLBACK AccessibilityEventRecorderWin::WinEventHookThunk(
119 HWINEVENTHOOK handle,
120 DWORD event,
121 HWND hwnd,
122 LONG obj_id,
123 LONG child_id,
124 DWORD event_thread,
125 DWORD event_time) {
126 if (instance_) {
127 instance_->OnWinEventHook(handle, event, hwnd, obj_id, child_id,
128 event_thread, event_time);
132 AccessibilityEventRecorderWin::AccessibilityEventRecorderWin(
133 BrowserAccessibilityManager* manager)
134 : AccessibilityEventRecorder(manager) {
135 CHECK(!instance_) << "There can be only one instance of"
136 << " WinAccessibilityEventMonitor at a time.";
137 instance_ = this;
138 win_event_hook_handle_ = SetWinEventHook(
139 EVENT_MIN,
140 EVENT_MAX,
141 GetModuleHandle(NULL),
142 &AccessibilityEventRecorderWin::WinEventHookThunk,
143 GetCurrentProcessId(),
144 0, // Hook all threads
145 WINEVENT_INCONTEXT);
146 CHECK(win_event_hook_handle_);
149 AccessibilityEventRecorderWin::~AccessibilityEventRecorderWin() {
150 UnhookWinEvent(win_event_hook_handle_);
151 instance_ = NULL;
154 void AccessibilityEventRecorderWin::OnWinEventHook(
155 HWINEVENTHOOK handle,
156 DWORD event,
157 HWND hwnd,
158 LONG obj_id,
159 LONG child_id,
160 DWORD event_thread,
161 DWORD event_time) {
162 base::win::ScopedComPtr<IAccessible> browser_accessible;
163 HRESULT hr = AccessibleObjectFromWindowWrapper(
164 hwnd,
165 obj_id,
166 IID_IAccessible,
167 reinterpret_cast<void**>(browser_accessible.Receive()));
168 if (!SUCCEEDED(hr)) {
169 // Note: our event hook will pick up some superfluous events we
170 // don't care about, so it's safe to just ignore these failures.
171 // Same below for other HRESULT checks.
172 VLOG(1) << "Ignoring result " << hr << " from AccessibleObjectFromWindow";
173 return;
176 base::win::ScopedVariant childid_variant(child_id);
177 base::win::ScopedComPtr<IDispatch> dispatch;
178 hr = browser_accessible->get_accChild(childid_variant, dispatch.Receive());
179 if (!SUCCEEDED(hr) || !dispatch) {
180 VLOG(1) << "Ignoring result " << hr << " and result " << dispatch
181 << " from get_accChild";
182 return;
185 base::win::ScopedComPtr<IAccessible> iaccessible;
186 hr = dispatch.QueryInterface(iaccessible.Receive());
187 if (!SUCCEEDED(hr)) {
188 VLOG(1) << "Ignoring result " << hr << " from QueryInterface";
189 return;
192 std::string event_str = AccessibilityEventToStringUTF8(event);
193 if (event_str.empty()) {
194 VLOG(1) << "Ignoring event " << event;
195 return;
198 base::win::ScopedVariant childid_self(CHILDID_SELF);
199 base::win::ScopedVariant role;
200 iaccessible->get_accRole(childid_self, role.Receive());
201 base::win::ScopedBstr name_bstr;
202 iaccessible->get_accName(childid_self, name_bstr.Receive());
203 base::win::ScopedBstr value_bstr;
204 iaccessible->get_accValue(childid_self, value_bstr.Receive());
205 base::win::ScopedVariant state;
206 iaccessible->get_accState(childid_self, state.Receive());
207 int ia_state = V_I4(state.ptr());
209 // Avoid flakiness. The "offscreen" state depends on whether the browser
210 // window is frontmost or not, and "hottracked" depends on whether the
211 // mouse cursor happens to be over the element.
212 ia_state &= (~STATE_SYSTEM_OFFSCREEN & ~STATE_SYSTEM_HOTTRACKED);
214 // The "readonly" state is set on almost every node and doesn't typically
215 // change, so filter it out to keep the output less verbose.
216 ia_state &= ~STATE_SYSTEM_READONLY;
218 AccessibleStates ia2_state = 0;
219 base::win::ScopedComPtr<IAccessible2> iaccessible2;
220 hr = QueryIAccessible2(iaccessible.get(), iaccessible2.Receive());
221 if (SUCCEEDED(hr))
222 iaccessible2->get_states(&ia2_state);
224 std::string log = base::StringPrintf(
225 "%s on role=%s", event_str.c_str(), RoleVariantToString(role).c_str());
226 if (name_bstr.Length() > 0)
227 log += base::StringPrintf(" name=\"%s\"", BstrToUTF8(name_bstr).c_str());
228 if (value_bstr.Length() > 0)
229 log += base::StringPrintf(" value=\"%s\"", BstrToUTF8(value_bstr).c_str());
230 log += " ";
231 log += base::UTF16ToUTF8(IAccessibleStateToString(ia_state));
232 log += " ";
233 log += base::UTF16ToUTF8(IAccessible2StateToString(ia2_state));
235 // For TEXT_REMOVED and TEXT_INSERTED events, query the text that was
236 // inserted or removed and include that in the log.
237 base::win::ScopedComPtr<IAccessibleText> accessible_text;
238 hr = QueryIAccessibleText(iaccessible.get(), accessible_text.Receive());
239 if (SUCCEEDED(hr)) {
240 if (event == IA2_EVENT_TEXT_REMOVED) {
241 IA2TextSegment old_text;
242 if (SUCCEEDED(accessible_text->get_oldText(&old_text))) {
243 log += base::StringPrintf(" old_text={'%s' start=%d end=%d}",
244 BstrToUTF8(old_text.text).c_str(),
245 old_text.start,
246 old_text.end);
249 if (event == IA2_EVENT_TEXT_INSERTED) {
250 IA2TextSegment new_text;
251 if (SUCCEEDED(accessible_text->get_newText(&new_text))) {
252 log += base::StringPrintf(" new_text={'%s' start=%d end=%d}",
253 BstrToUTF8(new_text.text).c_str(),
254 new_text.start,
255 new_text.end);
260 log = base::UTF16ToUTF8(
261 base::CollapseWhitespace(base::UTF8ToUTF16(log), true));
262 event_logs_.push_back(log);
265 HRESULT AccessibilityEventRecorderWin::AccessibleObjectFromWindowWrapper(
266 HWND hwnd, DWORD dw_id, REFIID riid, void** ppv_object) {
267 HRESULT hr = ::AccessibleObjectFromWindow(hwnd, dw_id, riid, ppv_object);
268 if (SUCCEEDED(hr))
269 return hr;
271 // The above call to ::AccessibleObjectFromWindow fails for unknown
272 // reasons every once in a while on the bots. Work around it by grabbing
273 // the object directly from the BrowserAccessibilityManager.
274 HWND accessibility_hwnd =
275 manager_->delegate()->AccessibilityGetAcceleratedWidget();
276 if (accessibility_hwnd != hwnd)
277 return E_FAIL;
279 IAccessible* obj = manager_->GetRoot()->ToBrowserAccessibilityWin();
280 obj->AddRef();
281 *ppv_object = obj;
282 return S_OK;
285 } // namespace content