| // Copyright (c) 2013 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 "win8/test/ui_automation_client.h" |
| |
| #include <atlbase.h> |
| #include <atlcom.h> |
| #include <oleauto.h> |
| #include <stdint.h> |
| #include <uiautomation.h> |
| |
| #include <algorithm> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/macros.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/thread_task_runner_handle.h" |
| #include "base/win/scoped_comptr.h" |
| #include "base/win/scoped_variant.h" |
| |
| namespace win8 { |
| namespace internal { |
| |
| // The guts of the UI automation client which runs on a dedicated thread in the |
| // multi-threaded COM apartment. An instance may be constructed on any thread, |
| // but Initialize() must be invoked on a thread in the MTA. |
| class UIAutomationClient::Context { |
| public: |
| // Returns a new instance ready for initialization and use on another thread. |
| static base::WeakPtr<Context> Create(); |
| |
| // Deletes the instance. |
| void DeleteOnAutomationThread(); |
| |
| // Initializes the context, invoking |init_callback| via |client_runner| when |
| // done. On success, |result_callback| will eventually be called after the |
| // window has been processed. On failure, this instance self-destructs after |
| // posting |init_callback|. |
| void Initialize( |
| scoped_refptr<base::SingleThreadTaskRunner> client_runner, |
| base::string16 class_name, |
| base::string16 item_name, |
| UIAutomationClient::InitializedCallback init_callback, |
| UIAutomationClient::ResultCallback result_callback); |
| |
| // Methods invoked by event handlers via weak pointers. |
| void HandleAutomationEvent( |
| base::win::ScopedComPtr<IUIAutomationElement> sender, |
| EVENTID eventId); |
| |
| private: |
| class EventHandler; |
| |
| // The only and only method that may be called from outside of the automation |
| // thread. |
| Context(); |
| ~Context(); |
| |
| HRESULT InstallWindowObserver(); |
| HRESULT RemoveWindowObserver(); |
| |
| void HandleWindowOpen( |
| const base::win::ScopedComPtr<IUIAutomationElement>& window); |
| void ProcessWindow( |
| const base::win::ScopedComPtr<IUIAutomationElement>& window); |
| HRESULT InvokeDesiredItem( |
| const base::win::ScopedComPtr<IUIAutomationElement>& element); |
| HRESULT GetInvokableItems( |
| const base::win::ScopedComPtr<IUIAutomationElement>& element, |
| std::vector<base::string16>* choices); |
| void CloseWindow(const base::win::ScopedComPtr<IUIAutomationElement>& window); |
| |
| base::ThreadChecker thread_checker_; |
| |
| // The loop on which the client itself lives. |
| scoped_refptr<base::SingleThreadTaskRunner> client_runner_; |
| |
| // The class name of the window for which the client waits. |
| base::string16 class_name_; |
| |
| // The name of the item to invoke. |
| base::string16 item_name_; |
| |
| // The consumer's result callback. |
| ResultCallback result_callback_; |
| |
| // The automation client. |
| base::win::ScopedComPtr<IUIAutomation> automation_; |
| |
| // A handler of Window open events. |
| base::win::ScopedComPtr<IUIAutomationEventHandler> event_handler_; |
| |
| // Weak pointers to the context are given to event handlers. |
| base::WeakPtrFactory<UIAutomationClient::Context> weak_ptr_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(Context); |
| }; |
| |
| class UIAutomationClient::Context::EventHandler |
| : public CComObjectRootEx<CComMultiThreadModel>, |
| public IUIAutomationEventHandler { |
| public: |
| BEGIN_COM_MAP(UIAutomationClient::Context::EventHandler) |
| COM_INTERFACE_ENTRY(IUIAutomationEventHandler) |
| END_COM_MAP() |
| |
| EventHandler(); |
| virtual ~EventHandler(); |
| |
| // Initializes the object with its parent UI automation client context's |
| // message loop and pointer. Events are dispatched back to the context on |
| // the given loop. |
| void Initialize( |
| const scoped_refptr<base::SingleThreadTaskRunner>& context_runner, |
| const base::WeakPtr<UIAutomationClient::Context>& context); |
| |
| // IUIAutomationEventHandler methods. |
| STDMETHOD(HandleAutomationEvent)(IUIAutomationElement* sender, |
| EVENTID eventId) override; |
| |
| private: |
| // The task runner for the UI automation client context. |
| scoped_refptr<base::SingleThreadTaskRunner> context_runner_; |
| |
| // The parent UI automation client context. |
| base::WeakPtr<UIAutomationClient::Context> context_; |
| |
| DISALLOW_COPY_AND_ASSIGN(EventHandler); |
| }; |
| |
| UIAutomationClient::Context::EventHandler::EventHandler() {} |
| |
| UIAutomationClient::Context::EventHandler::~EventHandler() {} |
| |
| void UIAutomationClient::Context::EventHandler::Initialize( |
| const scoped_refptr<base::SingleThreadTaskRunner>& context_runner, |
| const base::WeakPtr<UIAutomationClient::Context>& context) { |
| context_runner_ = context_runner; |
| context_ = context; |
| } |
| |
| HRESULT UIAutomationClient::Context::EventHandler::HandleAutomationEvent( |
| IUIAutomationElement* sender, |
| EVENTID eventId) { |
| // Event handlers are invoked on an arbitrary thread in the MTA. Send the |
| // event back to the main UI automation thread for processing. |
| context_runner_->PostTask( |
| FROM_HERE, |
| base::Bind(&UIAutomationClient::Context::HandleAutomationEvent, context_, |
| base::win::ScopedComPtr<IUIAutomationElement>(sender), |
| eventId)); |
| |
| return S_OK; |
| } |
| |
| base::WeakPtr<UIAutomationClient::Context> |
| UIAutomationClient::Context::Create() { |
| Context* context = new Context(); |
| return context->weak_ptr_factory_.GetWeakPtr(); |
| } |
| |
| void UIAutomationClient::Context::DeleteOnAutomationThread() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| delete this; |
| } |
| |
| UIAutomationClient::Context::Context() : weak_ptr_factory_(this) {} |
| |
| UIAutomationClient::Context::~Context() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| if (event_handler_.get()) { |
| event_handler_ = NULL; |
| HRESULT result = automation_->RemoveAllEventHandlers(); |
| LOG_IF(ERROR, FAILED(result)) << std::hex << result; |
| } |
| } |
| |
| void UIAutomationClient::Context::Initialize( |
| scoped_refptr<base::SingleThreadTaskRunner> client_runner, |
| base::string16 class_name, |
| base::string16 item_name, |
| UIAutomationClient::InitializedCallback init_callback, |
| UIAutomationClient::ResultCallback result_callback) { |
| // This and all other methods must be called on the automation thread. |
| DCHECK(!client_runner->BelongsToCurrentThread()); |
| // Bind the checker to this thread. |
| thread_checker_.DetachFromThread(); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| client_runner_ = client_runner; |
| class_name_ = class_name; |
| item_name_ = item_name; |
| result_callback_ = result_callback; |
| |
| HRESULT result = automation_.CreateInstance(CLSID_CUIAutomation, NULL, |
| CLSCTX_INPROC_SERVER); |
| if (FAILED(result) || !automation_.get()) |
| LOG(ERROR) << std::hex << result; |
| else |
| result = InstallWindowObserver(); |
| |
| // Tell the client that initialization is complete. |
| client_runner_->PostTask(FROM_HERE, base::Bind(init_callback, result)); |
| |
| // Self-destruct if the overall operation failed. |
| if (FAILED(result)) |
| delete this; |
| } |
| |
| // Installs the window observer. |
| HRESULT UIAutomationClient::Context::InstallWindowObserver() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK(automation_.get()); |
| DCHECK(!event_handler_.get()); |
| |
| HRESULT result = S_OK; |
| base::win::ScopedComPtr<IUIAutomationElement> root_element; |
| base::win::ScopedComPtr<IUIAutomationCacheRequest> cache_request; |
| |
| // Observe the opening of all windows. |
| result = automation_->GetRootElement(root_element.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| // Cache Window class, HWND, and window pattern for opened windows. |
| result = automation_->CreateCacheRequest(cache_request.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| cache_request->AddProperty(UIA_ClassNamePropertyId); |
| cache_request->AddProperty(UIA_NativeWindowHandlePropertyId); |
| |
| // Create the observer. |
| CComObject<EventHandler>* event_handler_obj = NULL; |
| result = CComObject<EventHandler>::CreateInstance(&event_handler_obj); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| event_handler_obj->Initialize(base::ThreadTaskRunnerHandle::Get(), |
| weak_ptr_factory_.GetWeakPtr()); |
| base::win::ScopedComPtr<IUIAutomationEventHandler> event_handler( |
| event_handler_obj); |
| |
| result = automation_->AddAutomationEventHandler( |
| UIA_Window_WindowOpenedEventId, root_element.get(), TreeScope_Descendants, |
| cache_request.get(), event_handler.get()); |
| |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| event_handler_ = event_handler; |
| return S_OK; |
| } |
| |
| // Removes this instance's window observer. |
| HRESULT UIAutomationClient::Context::RemoveWindowObserver() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK(automation_.get()); |
| DCHECK(event_handler_.get()); |
| |
| HRESULT result = S_OK; |
| base::win::ScopedComPtr<IUIAutomationElement> root_element; |
| |
| // The opening of all windows are observed. |
| result = automation_->GetRootElement(root_element.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| result = automation_->RemoveAutomationEventHandler( |
| UIA_Window_WindowOpenedEventId, root_element.get(), event_handler_.get()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| event_handler_ = NULL; |
| return S_OK; |
| } |
| |
| // Handles an automation event. If the event results in the processing for which |
| // this context was created, the context self-destructs after posting the |
| // results to the client. |
| void UIAutomationClient::Context::HandleAutomationEvent( |
| base::win::ScopedComPtr<IUIAutomationElement> sender, |
| EVENTID eventId) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| if (eventId == UIA_Window_WindowOpenedEventId) |
| HandleWindowOpen(sender); |
| } |
| |
| // Handles a WindowOpen event. If |window| is the one for which this instance is |
| // waiting, it is processed and this instance self-destructs after posting the |
| // results to the client. |
| void UIAutomationClient::Context::HandleWindowOpen( |
| const base::win::ScopedComPtr<IUIAutomationElement>& window) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| HRESULT hr = S_OK; |
| base::win::ScopedVariant var; |
| |
| hr = window->GetCachedPropertyValueEx(UIA_ClassNamePropertyId, TRUE, |
| var.Receive()); |
| if (FAILED(hr)) { |
| LOG(ERROR) << std::hex << hr; |
| return; |
| } |
| |
| if (V_VT(var.ptr()) != VT_BSTR) { |
| LOG(ERROR) << __FUNCTION__ |
| << " class name is not a BSTR: " << V_VT(var.ptr()); |
| return; |
| } |
| |
| base::string16 class_name(V_BSTR(var.ptr())); |
| |
| // Window class names are atoms, which are case-insensitive. Assume that |
| // the window in question only needs ASCII case-insensitivity. |
| if (class_name.size() == class_name_.size() && |
| std::equal(class_name.begin(), class_name.end(), class_name_.begin(), |
| base::CaseInsensitiveCompareASCII<wchar_t>())) { |
| RemoveWindowObserver(); |
| ProcessWindow(window); |
| } |
| } |
| |
| // Processes |window| by invoking the desired child item. If the item cannot be |
| // found or invoked, an attempt is made to get a list of all invokable children. |
| // The results are posted back to the client on |client_runner_|, and this |
| // instance self-destructs. |
| void UIAutomationClient::Context::ProcessWindow( |
| const base::win::ScopedComPtr<IUIAutomationElement>& window) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| HRESULT result = S_OK; |
| std::vector<base::string16> choices; |
| result = InvokeDesiredItem(window); |
| if (FAILED(result)) { |
| GetInvokableItems(window, &choices); |
| CloseWindow(window); |
| } |
| |
| client_runner_->PostTask(FROM_HERE, |
| base::Bind(result_callback_, result, choices)); |
| |
| // Self-destruct since there's nothing more to be done here. |
| delete this; |
| } |
| |
| // Invokes the desired child of |element|. |
| HRESULT UIAutomationClient::Context::InvokeDesiredItem( |
| const base::win::ScopedComPtr<IUIAutomationElement>& element) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| HRESULT result = S_OK; |
| base::win::ScopedVariant var; |
| base::win::ScopedComPtr<IUIAutomationCondition> invokable_condition; |
| base::win::ScopedComPtr<IUIAutomationCondition> item_name_condition; |
| base::win::ScopedComPtr<IUIAutomationCondition> control_view_condition; |
| base::win::ScopedComPtr<IUIAutomationCondition> condition; |
| base::win::ScopedComPtr<IUIAutomationCacheRequest> cache_request; |
| base::win::ScopedComPtr<IUIAutomationElement> target; |
| |
| // Search for an invokable element named item_name. |
| var.Set(true); |
| result = automation_->CreatePropertyCondition( |
| UIA_IsInvokePatternAvailablePropertyId, |
| var, |
| invokable_condition.Receive()); |
| var.Reset(); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| var.Set(item_name_.c_str()); |
| result = automation_->CreatePropertyCondition(UIA_NamePropertyId, |
| var, |
| item_name_condition.Receive()); |
| var.Reset(); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| result = automation_->get_ControlViewCondition( |
| control_view_condition.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| std::vector<IUIAutomationCondition*> conditions; |
| conditions.push_back(invokable_condition.get()); |
| conditions.push_back(item_name_condition.get()); |
| conditions.push_back(control_view_condition.get()); |
| result = automation_->CreateAndConditionFromNativeArray( |
| &conditions[0], conditions.size(), condition.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| // Cache invokable pattern for the item. |
| result = automation_->CreateCacheRequest(cache_request.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| cache_request->AddPattern(UIA_InvokePatternId); |
| |
| result = element->FindFirstBuildCache( |
| static_cast<TreeScope>(TreeScope_Children | TreeScope_Descendants), |
| condition.get(), cache_request.get(), target.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| // If the item was found, invoke it. |
| if (!target.get()) { |
| LOG(WARNING) << "Failed to find desired item to invoke."; |
| return E_FAIL; |
| } |
| |
| base::win::ScopedComPtr<IUIAutomationInvokePattern> invoker; |
| result = target->GetCachedPatternAs(UIA_InvokePatternId, invoker.iid(), |
| invoker.ReceiveVoid()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| result = invoker->Invoke(); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| return S_OK; |
| } |
| |
| // Populates |choices| with the names of all invokable children of |element|. |
| HRESULT UIAutomationClient::Context::GetInvokableItems( |
| const base::win::ScopedComPtr<IUIAutomationElement>& element, |
| std::vector<base::string16>* choices) { |
| DCHECK(choices); |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| HRESULT result = S_OK; |
| base::win::ScopedVariant var; |
| base::win::ScopedComPtr<IUIAutomationCondition> invokable_condition; |
| base::win::ScopedComPtr<IUIAutomationCondition> control_view_condition; |
| base::win::ScopedComPtr<IUIAutomationCondition> condition; |
| base::win::ScopedComPtr<IUIAutomationCacheRequest> cache_request; |
| base::win::ScopedComPtr<IUIAutomationElementArray> element_array; |
| base::win::ScopedComPtr<IUIAutomationElement> child_element; |
| |
| // Search for all invokable elements. |
| var.Set(true); |
| result = automation_->CreatePropertyCondition( |
| UIA_IsInvokePatternAvailablePropertyId, |
| var, |
| invokable_condition.Receive()); |
| var.Reset(); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| result = automation_->get_ControlViewCondition( |
| control_view_condition.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| result = automation_->CreateAndCondition(invokable_condition.get(), |
| control_view_condition.get(), |
| condition.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| // Cache item names. |
| result = automation_->CreateCacheRequest(cache_request.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| cache_request->AddProperty(UIA_NamePropertyId); |
| |
| result = element->FindAllBuildCache( |
| static_cast<TreeScope>(TreeScope_Children | TreeScope_Descendants), |
| condition.get(), cache_request.get(), element_array.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| if (!element_array.get()) { |
| LOG(ERROR) << "The window may have vanished."; |
| return S_OK; |
| } |
| |
| int num_elements = 0; |
| result = element_array->get_Length(&num_elements); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return result; |
| } |
| |
| choices->clear(); |
| choices->reserve(num_elements); |
| for (int i = 0; i < num_elements; ++i) { |
| child_element.Release(); |
| result = element_array->GetElement(i, child_element.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| continue; |
| } |
| result = child_element->GetCachedPropertyValueEx(UIA_NamePropertyId, TRUE, |
| var.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| continue; |
| } |
| if (V_VT(var.ptr()) != VT_BSTR) { |
| LOG(ERROR) << __FUNCTION__ << " name is not a BSTR: " << V_VT(var.ptr()); |
| continue; |
| } |
| choices->push_back(base::string16(V_BSTR(var.ptr()))); |
| var.Reset(); |
| } |
| |
| return result; |
| } |
| |
| // Closes the element |window| by sending it an escape key. |
| void UIAutomationClient::Context::CloseWindow( |
| const base::win::ScopedComPtr<IUIAutomationElement>& window) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| // It's tempting to get the Window pattern from |window| and invoke its Close |
| // method. Unfortunately, this doesn't work. Sending an escape key does the |
| // trick, though. |
| HRESULT result = S_OK; |
| base::win::ScopedVariant var; |
| |
| result = window->GetCachedPropertyValueEx( |
| UIA_NativeWindowHandlePropertyId, |
| TRUE, |
| var.Receive()); |
| if (FAILED(result)) { |
| LOG(ERROR) << std::hex << result; |
| return; |
| } |
| |
| if (V_VT(var.ptr()) != VT_I4) { |
| LOG(ERROR) << __FUNCTION__ |
| << " window handle is not an int: " << V_VT(var.ptr()); |
| return; |
| } |
| |
| HWND handle = reinterpret_cast<HWND>(V_I4(var.ptr())); |
| |
| uint32_t scan_code = MapVirtualKey(VK_ESCAPE, MAPVK_VK_TO_VSC); |
| PostMessage(handle, WM_KEYDOWN, VK_ESCAPE, |
| MAKELPARAM(1, scan_code)); |
| PostMessage(handle, WM_KEYUP, VK_ESCAPE, |
| MAKELPARAM(1, scan_code | KF_REPEAT | KF_UP)); |
| } |
| |
| UIAutomationClient::UIAutomationClient() |
| : automation_thread_("UIAutomation") {} |
| |
| UIAutomationClient::~UIAutomationClient() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| // context_ is still valid when the caller destroys the instance before the |
| // callback(s) have fired. In this case, delete the context on the automation |
| // thread before joining with it. |
| automation_thread_.message_loop()->PostTask( |
| FROM_HERE, |
| base::Bind(&UIAutomationClient::Context::DeleteOnAutomationThread, |
| context_)); |
| } |
| |
| void UIAutomationClient::Begin(const wchar_t* class_name, |
| const base::string16& item_name, |
| const InitializedCallback& init_callback, |
| const ResultCallback& result_callback) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK_EQ(context_.get(), static_cast<Context*>(NULL)); |
| |
| // Start the automation thread and initialize our automation client on it. |
| context_ = Context::Create(); |
| automation_thread_.init_com_with_mta(true); |
| automation_thread_.Start(); |
| automation_thread_.message_loop()->PostTask( |
| FROM_HERE, |
| base::Bind(&UIAutomationClient::Context::Initialize, |
| context_, |
| base::ThreadTaskRunnerHandle::Get(), |
| base::string16(class_name), |
| item_name, |
| init_callback, |
| result_callback)); |
| } |
| |
| } // namespace internal |
| } // namespace win8 |