| // Copyright 2014 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 <oleacc.h> |
| #include <stdint.h> |
| |
| #include <string> |
| |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/win/scoped_bstr.h" |
| #include "base/win/scoped_comptr.h" |
| #include "base/win/scoped_variant.h" |
| #include "content/browser/accessibility/accessibility_tree_formatter_utils_win.h" |
| #include "content/browser/accessibility/browser_accessibility_manager.h" |
| #include "content/browser/accessibility/browser_accessibility_win.h" |
| #include "third_party/iaccessible2/ia2_api_all.h" |
| #include "ui/base/win/atl_module.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| std::string RoleVariantToString(const base::win::ScopedVariant& role) { |
| if (role.type() == VT_I4) { |
| return base::UTF16ToUTF8(IAccessibleRoleToString(V_I4(role.ptr()))); |
| } else if (role.type() == VT_BSTR) { |
| return base::UTF16ToUTF8( |
| base::string16(V_BSTR(role.ptr()), SysStringLen(V_BSTR(role.ptr())))); |
| } |
| return std::string(); |
| } |
| |
| HRESULT QueryIAccessible2(IAccessible* accessible, IAccessible2** accessible2) { |
| base::win::ScopedComPtr<IServiceProvider> service_provider; |
| HRESULT hr = accessible->QueryInterface(service_provider.Receive()); |
| return SUCCEEDED(hr) ? |
| service_provider->QueryService(IID_IAccessible2, accessible2) : hr; |
| } |
| |
| HRESULT QueryIAccessibleText(IAccessible* accessible, |
| IAccessibleText** accessible_text) { |
| base::win::ScopedComPtr<IServiceProvider> service_provider; |
| HRESULT hr = accessible->QueryInterface(service_provider.Receive()); |
| return SUCCEEDED(hr) ? |
| service_provider->QueryService(IID_IAccessibleText, accessible_text) : hr; |
| } |
| |
| std::string BstrToUTF8(BSTR bstr) { |
| base::string16 str16(bstr, SysStringLen(bstr)); |
| |
| // IAccessibleText returns the text you get by appending all static text |
| // children, with an "embedded object character" for each non-text child. |
| // Pretty-print the embedded object character as <obj> so that test output |
| // is human-readable. |
| base::StringPiece16 embedded_character( |
| &BrowserAccessibilityComWin::kEmbeddedCharacter, 1); |
| base::ReplaceChars(str16, embedded_character, L"<obj>", &str16); |
| |
| return base::UTF16ToUTF8(str16); |
| } |
| |
| std::string AccessibilityEventToStringUTF8(int32_t event_id) { |
| return base::UTF16ToUTF8(AccessibilityEventToString(event_id)); |
| } |
| |
| } // namespace |
| |
| class AccessibilityEventRecorderWin : public AccessibilityEventRecorder { |
| public: |
| explicit AccessibilityEventRecorderWin(BrowserAccessibilityManager* manager); |
| ~AccessibilityEventRecorderWin() override; |
| |
| // Callback registered by SetWinEventHook. Just calls OnWinEventHook. |
| static void CALLBACK WinEventHookThunk( |
| HWINEVENTHOOK handle, |
| DWORD event, |
| HWND hwnd, |
| LONG obj_id, |
| LONG child_id, |
| DWORD event_thread, |
| DWORD event_time); |
| |
| private: |
| // Called by the thunk registered by SetWinEventHook. Retrives accessibility |
| // info about the node the event was fired on and appends a string to |
| // the event log. |
| void OnWinEventHook(HWINEVENTHOOK handle, |
| DWORD event, |
| HWND hwnd, |
| LONG obj_id, |
| LONG child_id, |
| DWORD event_thread, |
| DWORD event_time); |
| |
| // Wrapper around AccessibleObjectFromWindow because the function call |
| // inexplicably flakes sometimes on build/trybots. |
| HRESULT AccessibleObjectFromWindowWrapper( |
| HWND hwnd, DWORD dwId, REFIID riid, void **ppvObject); |
| |
| HWINEVENTHOOK win_event_hook_handle_; |
| static AccessibilityEventRecorderWin* instance_; |
| }; |
| |
| // static |
| AccessibilityEventRecorderWin* |
| AccessibilityEventRecorderWin::instance_ = nullptr; |
| |
| // static |
| AccessibilityEventRecorder* AccessibilityEventRecorder::Create( |
| BrowserAccessibilityManager* manager) { |
| return new AccessibilityEventRecorderWin(manager); |
| } |
| |
| // static |
| void CALLBACK AccessibilityEventRecorderWin::WinEventHookThunk( |
| HWINEVENTHOOK handle, |
| DWORD event, |
| HWND hwnd, |
| LONG obj_id, |
| LONG child_id, |
| DWORD event_thread, |
| DWORD event_time) { |
| if (instance_) { |
| instance_->OnWinEventHook(handle, event, hwnd, obj_id, child_id, |
| event_thread, event_time); |
| } |
| } |
| |
| AccessibilityEventRecorderWin::AccessibilityEventRecorderWin( |
| BrowserAccessibilityManager* manager) |
| : AccessibilityEventRecorder(manager) { |
| CHECK(!instance_) << "There can be only one instance of" |
| << " WinAccessibilityEventMonitor at a time."; |
| instance_ = this; |
| win_event_hook_handle_ = SetWinEventHook( |
| EVENT_MIN, |
| EVENT_MAX, |
| GetModuleHandle(NULL), |
| &AccessibilityEventRecorderWin::WinEventHookThunk, |
| GetCurrentProcessId(), |
| 0, // Hook all threads |
| WINEVENT_INCONTEXT); |
| CHECK(win_event_hook_handle_); |
| } |
| |
| AccessibilityEventRecorderWin::~AccessibilityEventRecorderWin() { |
| UnhookWinEvent(win_event_hook_handle_); |
| instance_ = NULL; |
| } |
| |
| void AccessibilityEventRecorderWin::OnWinEventHook( |
| HWINEVENTHOOK handle, |
| DWORD event, |
| HWND hwnd, |
| LONG obj_id, |
| LONG child_id, |
| DWORD event_thread, |
| DWORD event_time) { |
| base::win::ScopedComPtr<IAccessible> browser_accessible; |
| HRESULT hr = AccessibleObjectFromWindowWrapper( |
| hwnd, |
| obj_id, |
| IID_IAccessible, |
| reinterpret_cast<void**>(browser_accessible.Receive())); |
| if (!SUCCEEDED(hr)) { |
| // Note: our event hook will pick up some superfluous events we |
| // don't care about, so it's safe to just ignore these failures. |
| // Same below for other HRESULT checks. |
| VLOG(1) << "Ignoring result " << hr << " from AccessibleObjectFromWindow"; |
| return; |
| } |
| |
| base::win::ScopedVariant childid_variant(child_id); |
| base::win::ScopedComPtr<IDispatch> dispatch; |
| hr = browser_accessible->get_accChild(childid_variant, dispatch.Receive()); |
| if (!SUCCEEDED(hr) || !dispatch) { |
| VLOG(1) << "Ignoring result " << hr << " and result " << dispatch |
| << " from get_accChild"; |
| return; |
| } |
| |
| base::win::ScopedComPtr<IAccessible> iaccessible; |
| hr = dispatch.CopyTo(iaccessible.Receive()); |
| if (!SUCCEEDED(hr)) { |
| VLOG(1) << "Ignoring result " << hr << " from QueryInterface"; |
| return; |
| } |
| |
| std::string event_str = AccessibilityEventToStringUTF8(event); |
| if (event_str.empty()) { |
| VLOG(1) << "Ignoring event " << event; |
| return; |
| } |
| |
| base::win::ScopedVariant childid_self(CHILDID_SELF); |
| base::win::ScopedVariant role; |
| iaccessible->get_accRole(childid_self, role.Receive()); |
| base::win::ScopedBstr name_bstr; |
| iaccessible->get_accName(childid_self, name_bstr.Receive()); |
| base::win::ScopedBstr value_bstr; |
| iaccessible->get_accValue(childid_self, value_bstr.Receive()); |
| base::win::ScopedVariant state; |
| iaccessible->get_accState(childid_self, state.Receive()); |
| int ia_state = V_I4(state.ptr()); |
| |
| // Avoid flakiness. Events fired on a WINDOW are out of the control |
| // of a test. |
| if (role.type() == VT_I4 && ROLE_SYSTEM_WINDOW == V_I4(role.ptr())) { |
| VLOG(1) << "Ignoring event " << event << " on ROLE_SYSTEM_WINDOW"; |
| return; |
| } |
| |
| // Avoid flakiness. The "offscreen" state depends on whether the browser |
| // window is frontmost or not, and "hottracked" depends on whether the |
| // mouse cursor happens to be over the element. |
| ia_state &= (~STATE_SYSTEM_OFFSCREEN & ~STATE_SYSTEM_HOTTRACKED); |
| |
| // The "readonly" state is set on almost every node and doesn't typically |
| // change, so filter it out to keep the output less verbose. |
| ia_state &= ~STATE_SYSTEM_READONLY; |
| |
| AccessibleStates ia2_state = 0; |
| base::win::ScopedComPtr<IAccessible2> iaccessible2; |
| hr = QueryIAccessible2(iaccessible.Get(), iaccessible2.Receive()); |
| if (SUCCEEDED(hr)) |
| iaccessible2->get_states(&ia2_state); |
| |
| std::string log = base::StringPrintf( |
| "%s on role=%s", event_str.c_str(), RoleVariantToString(role).c_str()); |
| if (name_bstr.Length() > 0) |
| log += base::StringPrintf(" name=\"%s\"", BstrToUTF8(name_bstr).c_str()); |
| if (value_bstr.Length() > 0) |
| log += base::StringPrintf(" value=\"%s\"", BstrToUTF8(value_bstr).c_str()); |
| log += " "; |
| log += base::UTF16ToUTF8(IAccessibleStateToString(ia_state)); |
| log += " "; |
| log += base::UTF16ToUTF8(IAccessible2StateToString(ia2_state)); |
| |
| // For TEXT_REMOVED and TEXT_INSERTED events, query the text that was |
| // inserted or removed and include that in the log. |
| base::win::ScopedComPtr<IAccessibleText> accessible_text; |
| hr = QueryIAccessibleText(iaccessible.Get(), accessible_text.Receive()); |
| if (SUCCEEDED(hr)) { |
| if (event == IA2_EVENT_TEXT_REMOVED) { |
| IA2TextSegment old_text; |
| if (SUCCEEDED(accessible_text->get_oldText(&old_text))) { |
| log += base::StringPrintf(" old_text={'%s' start=%d end=%d}", |
| BstrToUTF8(old_text.text).c_str(), |
| old_text.start, |
| old_text.end); |
| } |
| } |
| if (event == IA2_EVENT_TEXT_INSERTED) { |
| IA2TextSegment new_text; |
| if (SUCCEEDED(accessible_text->get_newText(&new_text))) { |
| log += base::StringPrintf(" new_text={'%s' start=%d end=%d}", |
| BstrToUTF8(new_text.text).c_str(), |
| new_text.start, |
| new_text.end); |
| } |
| } |
| } |
| |
| log = base::UTF16ToUTF8( |
| base::CollapseWhitespace(base::UTF8ToUTF16(log), true)); |
| event_logs_.push_back(log); |
| } |
| |
| HRESULT AccessibilityEventRecorderWin::AccessibleObjectFromWindowWrapper( |
| HWND hwnd, DWORD dw_id, REFIID riid, void** ppv_object) { |
| HRESULT hr = ::AccessibleObjectFromWindow(hwnd, dw_id, riid, ppv_object); |
| if (SUCCEEDED(hr)) |
| return hr; |
| |
| // The above call to ::AccessibleObjectFromWindow fails for unknown |
| // reasons every once in a while on the bots. Work around it by grabbing |
| // the object directly from the BrowserAccessibilityManager. |
| HWND accessibility_hwnd = |
| manager_->delegate()->AccessibilityGetAcceleratedWidget(); |
| if (accessibility_hwnd != hwnd) |
| return E_FAIL; |
| |
| IAccessible* obj = ToBrowserAccessibilityComWin(manager_->GetRoot()); |
| obj->AddRef(); |
| *ppv_object = obj; |
| return S_OK; |
| } |
| |
| } // namespace content |