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"
11 #include "base/strings/string_number_conversions.h"
12 #include "base/strings/string_piece.h"
13 #include "base/strings/string_util.h"
14 #include "base/strings/stringprintf.h"
15 #include "base/strings/utf_string_conversions.h"
16 #include "base/win/scoped_bstr.h"
17 #include "base/win/scoped_comptr.h"
18 #include "base/win/scoped_variant.h"
19 #include "content/browser/accessibility/accessibility_tree_formatter_utils_win.h"
20 #include "content/browser/accessibility/browser_accessibility_manager.h"
21 #include "content/browser/accessibility/browser_accessibility_win.h"
22 #include "third_party/iaccessible2/ia2_api_all.h"
23 #include "ui/base/win/atl_module.h"
29 std::string
RoleVariantToString(const base::win::ScopedVariant
& role
) {
30 if (role
.type() == VT_I4
) {
31 return base::UTF16ToUTF8(IAccessibleRoleToString(V_I4(role
.ptr())));
32 } else if (role
.type() == VT_BSTR
) {
33 return base::UTF16ToUTF8(
34 base::string16(V_BSTR(role
.ptr()), SysStringLen(V_BSTR(role
.ptr()))));
39 HRESULT
QueryIAccessible2(IAccessible
* accessible
, IAccessible2
** accessible2
) {
40 base::win::ScopedComPtr
<IServiceProvider
> service_provider
;
41 HRESULT hr
= accessible
->QueryInterface(service_provider
.Receive());
42 return SUCCEEDED(hr
) ?
43 service_provider
->QueryService(IID_IAccessible2
, accessible2
) : hr
;
46 HRESULT
QueryIAccessibleText(IAccessible
* accessible
,
47 IAccessibleText
** accessible_text
) {
48 base::win::ScopedComPtr
<IServiceProvider
> service_provider
;
49 HRESULT hr
= accessible
->QueryInterface(service_provider
.Receive());
50 return SUCCEEDED(hr
) ?
51 service_provider
->QueryService(IID_IAccessibleText
, accessible_text
) : hr
;
54 std::string
BstrToUTF8(BSTR bstr
) {
55 base::string16
str16(bstr
, SysStringLen(bstr
));
57 // IAccessibleText returns the text you get by appending all static text
58 // children, with an "embedded object character" for each non-text child.
59 // Pretty-print the embedded object character as <obj> so that test output
61 base::StringPiece16
embedded_character(
62 &BrowserAccessibilityWin::kEmbeddedCharacter
, 1);
63 base::ReplaceChars(str16
, embedded_character
, L
"<obj>", &str16
);
65 return base::UTF16ToUTF8(str16
);
68 std::string
AccessibilityEventToStringUTF8(int32 event_id
) {
69 return base::UTF16ToUTF8(AccessibilityEventToString(event_id
));
74 class AccessibilityEventRecorderWin
: public AccessibilityEventRecorder
{
76 explicit AccessibilityEventRecorderWin(BrowserAccessibilityManager
* manager
);
77 ~AccessibilityEventRecorderWin() override
;
79 // Callback registered by SetWinEventHook. Just calls OnWinEventHook.
80 static void CALLBACK
WinEventHookThunk(
90 // Called by the thunk registered by SetWinEventHook. Retrives accessibility
91 // info about the node the event was fired on and appends a string to
93 void OnWinEventHook(HWINEVENTHOOK handle
,
101 // Wrapper around AccessibleObjectFromWindow because the function call
102 // inexplicably flakes sometimes on build/trybots.
103 HRESULT
AccessibleObjectFromWindowWrapper(
104 HWND hwnd
, DWORD dwId
, REFIID riid
, void **ppvObject
);
106 HWINEVENTHOOK win_event_hook_handle_
;
107 static AccessibilityEventRecorderWin
* instance_
;
111 AccessibilityEventRecorderWin
*
112 AccessibilityEventRecorderWin::instance_
= nullptr;
115 AccessibilityEventRecorder
* AccessibilityEventRecorder::Create(
116 BrowserAccessibilityManager
* manager
) {
117 return new AccessibilityEventRecorderWin(manager
);
121 void CALLBACK
AccessibilityEventRecorderWin::WinEventHookThunk(
122 HWINEVENTHOOK handle
,
130 instance_
->OnWinEventHook(handle
, event
, hwnd
, obj_id
, child_id
,
131 event_thread
, event_time
);
135 AccessibilityEventRecorderWin::AccessibilityEventRecorderWin(
136 BrowserAccessibilityManager
* manager
)
137 : AccessibilityEventRecorder(manager
) {
138 CHECK(!instance_
) << "There can be only one instance of"
139 << " WinAccessibilityEventMonitor at a time.";
141 win_event_hook_handle_
= SetWinEventHook(
144 GetModuleHandle(NULL
),
145 &AccessibilityEventRecorderWin::WinEventHookThunk
,
146 GetCurrentProcessId(),
147 0, // Hook all threads
149 CHECK(win_event_hook_handle_
);
152 AccessibilityEventRecorderWin::~AccessibilityEventRecorderWin() {
153 UnhookWinEvent(win_event_hook_handle_
);
157 void AccessibilityEventRecorderWin::OnWinEventHook(
158 HWINEVENTHOOK handle
,
165 base::win::ScopedComPtr
<IAccessible
> browser_accessible
;
166 HRESULT hr
= AccessibleObjectFromWindowWrapper(
170 reinterpret_cast<void**>(browser_accessible
.Receive()));
171 if (!SUCCEEDED(hr
)) {
172 // Note: our event hook will pick up some superfluous events we
173 // don't care about, so it's safe to just ignore these failures.
174 // Same below for other HRESULT checks.
175 VLOG(1) << "Ignoring result " << hr
<< " from AccessibleObjectFromWindow";
179 base::win::ScopedVariant
childid_variant(child_id
);
180 base::win::ScopedComPtr
<IDispatch
> dispatch
;
181 hr
= browser_accessible
->get_accChild(childid_variant
, dispatch
.Receive());
182 if (!SUCCEEDED(hr
) || !dispatch
) {
183 VLOG(1) << "Ignoring result " << hr
<< " and result " << dispatch
184 << " from get_accChild";
188 base::win::ScopedComPtr
<IAccessible
> iaccessible
;
189 hr
= dispatch
.QueryInterface(iaccessible
.Receive());
190 if (!SUCCEEDED(hr
)) {
191 VLOG(1) << "Ignoring result " << hr
<< " from QueryInterface";
195 std::string event_str
= AccessibilityEventToStringUTF8(event
);
196 if (event_str
.empty()) {
197 VLOG(1) << "Ignoring event " << event
;
201 base::win::ScopedVariant
childid_self(CHILDID_SELF
);
202 base::win::ScopedVariant role
;
203 iaccessible
->get_accRole(childid_self
, role
.Receive());
204 base::win::ScopedBstr name_bstr
;
205 iaccessible
->get_accName(childid_self
, name_bstr
.Receive());
206 base::win::ScopedBstr value_bstr
;
207 iaccessible
->get_accValue(childid_self
, value_bstr
.Receive());
208 base::win::ScopedVariant state
;
209 iaccessible
->get_accState(childid_self
, state
.Receive());
210 int ia_state
= V_I4(state
.ptr());
212 // Avoid flakiness. Events fired on a WINDOW are out of the control
214 if (role
.type() == VT_I4
&& ROLE_SYSTEM_WINDOW
== V_I4(role
.ptr())) {
215 VLOG(1) << "Ignoring event " << event
<< " on ROLE_SYSTEM_WINDOW";
219 // Avoid flakiness. The "offscreen" state depends on whether the browser
220 // window is frontmost or not, and "hottracked" depends on whether the
221 // mouse cursor happens to be over the element.
222 ia_state
&= (~STATE_SYSTEM_OFFSCREEN
& ~STATE_SYSTEM_HOTTRACKED
);
224 // The "readonly" state is set on almost every node and doesn't typically
225 // change, so filter it out to keep the output less verbose.
226 ia_state
&= ~STATE_SYSTEM_READONLY
;
228 AccessibleStates ia2_state
= 0;
229 base::win::ScopedComPtr
<IAccessible2
> iaccessible2
;
230 hr
= QueryIAccessible2(iaccessible
.get(), iaccessible2
.Receive());
232 iaccessible2
->get_states(&ia2_state
);
234 std::string log
= base::StringPrintf(
235 "%s on role=%s", event_str
.c_str(), RoleVariantToString(role
).c_str());
236 if (name_bstr
.Length() > 0)
237 log
+= base::StringPrintf(" name=\"%s\"", BstrToUTF8(name_bstr
).c_str());
238 if (value_bstr
.Length() > 0)
239 log
+= base::StringPrintf(" value=\"%s\"", BstrToUTF8(value_bstr
).c_str());
241 log
+= base::UTF16ToUTF8(IAccessibleStateToString(ia_state
));
243 log
+= base::UTF16ToUTF8(IAccessible2StateToString(ia2_state
));
245 // For TEXT_REMOVED and TEXT_INSERTED events, query the text that was
246 // inserted or removed and include that in the log.
247 base::win::ScopedComPtr
<IAccessibleText
> accessible_text
;
248 hr
= QueryIAccessibleText(iaccessible
.get(), accessible_text
.Receive());
250 if (event
== IA2_EVENT_TEXT_REMOVED
) {
251 IA2TextSegment old_text
;
252 if (SUCCEEDED(accessible_text
->get_oldText(&old_text
))) {
253 log
+= base::StringPrintf(" old_text={'%s' start=%d end=%d}",
254 BstrToUTF8(old_text
.text
).c_str(),
259 if (event
== IA2_EVENT_TEXT_INSERTED
) {
260 IA2TextSegment new_text
;
261 if (SUCCEEDED(accessible_text
->get_newText(&new_text
))) {
262 log
+= base::StringPrintf(" new_text={'%s' start=%d end=%d}",
263 BstrToUTF8(new_text
.text
).c_str(),
270 log
= base::UTF16ToUTF8(
271 base::CollapseWhitespace(base::UTF8ToUTF16(log
), true));
272 event_logs_
.push_back(log
);
275 HRESULT
AccessibilityEventRecorderWin::AccessibleObjectFromWindowWrapper(
276 HWND hwnd
, DWORD dw_id
, REFIID riid
, void** ppv_object
) {
277 HRESULT hr
= ::AccessibleObjectFromWindow(hwnd
, dw_id
, riid
, ppv_object
);
281 // The above call to ::AccessibleObjectFromWindow fails for unknown
282 // reasons every once in a while on the bots. Work around it by grabbing
283 // the object directly from the BrowserAccessibilityManager.
284 HWND accessibility_hwnd
=
285 manager_
->delegate()->AccessibilityGetAcceleratedWidget();
286 if (accessibility_hwnd
!= hwnd
)
289 IAccessible
* obj
= manager_
->GetRoot()->ToBrowserAccessibilityWin();
295 } // namespace content