diff options
author | grt@chromium.org <grt@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-01-17 16:21:01 +0000 |
---|---|---|
committer | grt@chromium.org <grt@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-01-17 16:21:01 +0000 |
commit | e1eeaf4fd62e470d1ab9f6b670c3cf74eda8d435 (patch) | |
tree | 8cc99739ba4d4f306e10c78bf909214bd0f01b77 /win8 | |
parent | d28c6b2c72a373702f4604af540b4c7b10fd2c7d (diff) | |
download | chromium_src-e1eeaf4fd62e470d1ab9f6b670c3cf74eda8d435.zip chromium_src-e1eeaf4fd62e470d1ab9f6b670c3cf74eda8d435.tar.gz chromium_src-e1eeaf4fd62e470d1ab9f6b670c3cf74eda8d435.tar.bz2 |
A mechanism to set the default handler for a URL protocol on Windows 8.
This change introduces OpenWithDialogController, which provides an asynchronous as well as a synchronous interface to setting a default protocol handler on Windows 8.
BUG=154081
Review URL: https://chromiumcodereview.appspot.com/11863016
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@177417 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'win8')
-rw-r--r-- | win8/test/open_with_dialog_async.cc | 121 | ||||
-rw-r--r-- | win8/test/open_with_dialog_async.h | 35 | ||||
-rw-r--r-- | win8/test/open_with_dialog_controller.cc | 291 | ||||
-rw-r--r-- | win8/test/open_with_dialog_controller.h | 61 | ||||
-rw-r--r-- | win8/test/ui_automation_client.cc | 632 | ||||
-rw-r--r-- | win8/test/ui_automation_client.h | 70 | ||||
-rw-r--r-- | win8/win8.gyp | 15 |
7 files changed, 1225 insertions, 0 deletions
diff --git a/win8/test/open_with_dialog_async.cc b/win8/test/open_with_dialog_async.cc new file mode 100644 index 0000000..f491149 --- /dev/null +++ b/win8/test/open_with_dialog_async.cc @@ -0,0 +1,121 @@ +// 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. + +// Implementation for the asynchronous interface to the Windows shell +// SHOpenWithDialog function. The call is made on a dedicated UI thread in a +// single-threaded apartment. + +#include "win8/test/open_with_dialog_async.h" + +#include <shlobj.h> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/location.h" +#include "base/memory/ref_counted.h" +#include "base/single_thread_task_runner.h" +#include "base/thread_task_runner_handle.h" +#include "base/threading/platform_thread.h" +#include "base/threading/thread.h" +#include "base/win/windows_version.h" + +namespace win8 { + +namespace { + +struct OpenWithContext { + OpenWithContext( + HWND parent_window_in, + const string16& file_name_in, + const string16& file_type_class_in, + int open_as_info_flags_in, + const scoped_refptr<base::SingleThreadTaskRunner>& client_runner_in, + const OpenWithDialogCallback& callback_in); + ~OpenWithContext(); + + base::Thread thread; + HWND parent_window; + string16 file_name; + string16 file_type_class; + int open_as_info_flags; + scoped_refptr<base::SingleThreadTaskRunner> client_runner; + OpenWithDialogCallback callback; +}; + +OpenWithContext::OpenWithContext( + HWND parent_window_in, + const string16& file_name_in, + const string16& file_type_class_in, + int open_as_info_flags_in, + const scoped_refptr<base::SingleThreadTaskRunner>& client_runner_in, + const OpenWithDialogCallback& callback_in) + : thread("OpenWithDialog"), + parent_window(parent_window_in), + file_name(file_name_in), + file_type_class(file_type_class_in), + open_as_info_flags(open_as_info_flags_in), + client_runner(client_runner_in), + callback(callback_in) { + thread.init_com_with_mta(false); + thread.Start(); +} + +OpenWithContext::~OpenWithContext() {} + +// Runs the caller-provided |callback| with the result of the call to +// SHOpenWithDialog on the caller's initial thread. +void OnOpenWithDialogDone(OpenWithContext* context, HRESULT result) { + DCHECK(context->client_runner->BelongsToCurrentThread()); + OpenWithDialogCallback callback(context->callback); + + // Join with the thread. + delete context; + + // Run the client's callback. + callback.Run(result); +} + +// Calls SHOpenWithDialog (blocking), and returns the result back to the client +// thread. +void OpenWithDialogTask(OpenWithContext* context) { + DCHECK_EQ(context->thread.thread_id(), base::PlatformThread::CurrentId()); + OPENASINFO open_as_info = { + context->file_name.c_str(), + context->file_type_class.c_str(), + context->open_as_info_flags + }; + + HRESULT result = ::SHOpenWithDialog(context->parent_window, &open_as_info); + + // Bounce back to the calling thread to release resources and deliver the + // callback. + if (!context->client_runner->PostTask( + FROM_HERE, + base::Bind(&OnOpenWithDialogDone, context, result))) { + // The calling thread has gone away. There's nothing to be done but leak. + // In practice this is only likely to happen at shutdown, so there isn't + // much of a concern that it'll happen in the real world. + DLOG(ERROR) << "leaking OpenWith thread; result = " << std::hex << result; + } +} + +} // namespace + +void OpenWithDialogAsync( + HWND parent_window, + const string16& file_name, + const string16& file_type_class, + int open_as_info_flags, + const OpenWithDialogCallback& callback) { + DCHECK_GE(base::win::GetVersion(), base::win::VERSION_VISTA); + OpenWithContext* context = + new OpenWithContext(parent_window, file_name, file_type_class, + open_as_info_flags, + base::ThreadTaskRunnerHandle::Get(), callback); + context->thread.message_loop()->PostTask( + FROM_HERE, + base::Bind(&OpenWithDialogTask, context)); +} + +} // namespace win8 diff --git a/win8/test/open_with_dialog_async.h b/win8/test/open_with_dialog_async.h new file mode 100644 index 0000000..dadc9ca --- /dev/null +++ b/win8/test/open_with_dialog_async.h @@ -0,0 +1,35 @@ +// 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. + +#ifndef WIN8_TEST_OPEN_WITH_DIALOG_ASYNC_H_ +#define WIN8_TEST_OPEN_WITH_DIALOG_ASYNC_H_ + +#include <windows.h> + +#include "base/callback_forward.h" +#include "base/string16.h" + +namespace win8 { + +// Expected HRESULTS: +// S_OK - A choice was made. +// HRESULT_FROM_WIN32(ERROR_CANCELLED) - The dialog was dismissed. +// HRESULT_FROM_WIN32(RPC_S_CALL_FAILED) - OpenWith.exe died. +typedef base::Callback<void(HRESULT)> OpenWithDialogCallback; + +// Calls SHOpenWithDialog on a dedicated thread, returning the result to the +// caller via |callback| on the current thread. The Windows SHOpenWithDialog +// function blocks until the user makes a choice or dismisses the dialog (there +// is no natural timeout nor a means by which it can be cancelled). Note that +// the dedicated thread will be leaked if the calling thread's message loop goes +// away before the interaction completes. +void OpenWithDialogAsync(HWND parent_window, + const string16& file_name, + const string16& file_type_class, + int open_as_info_flags, + const OpenWithDialogCallback& callback); + +} // namespace win8 + +#endif // WIN8_TEST_OPEN_WITH_DIALOG_ASYNC_H_ diff --git a/win8/test/open_with_dialog_controller.cc b/win8/test/open_with_dialog_controller.cc new file mode 100644 index 0000000..7a63dd0 --- /dev/null +++ b/win8/test/open_with_dialog_controller.cc @@ -0,0 +1,291 @@ +// 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/open_with_dialog_controller.h" + +#include <shlobj.h> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop.h" +#include "base/run_loop.h" +#include "base/thread_task_runner_handle.h" +#include "base/threading/thread_checker.h" +#include "base/win/windows_version.h" +#include "win8/test/open_with_dialog_async.h" +#include "win8/test/ui_automation_client.h" + +namespace win8 { + +namespace { + +const int kControllerTimeoutSeconds = 5; +const wchar_t kShellFlyoutClassName[] = L"Shell_Flyout"; + +// A callback invoked with the OpenWithDialogController's results. Said results +// are copied to |result_out| and |choices_out| and then |closure| is invoked. +// This function is in support of OpenWithDialogController::RunSynchronously. +void OnMakeDefaultComplete( + const base::Closure& closure, + HRESULT* result_out, + std::vector<string16>* choices_out, + HRESULT hr, + std::vector<string16> choices) { + *result_out = hr; + *choices_out = choices; + closure.Run(); +} + +} // namespace + +// Lives on the main thread and is owned by a controller. May outlive the +// controller (see Orphan). +class OpenWithDialogController::Context { + public: + Context(); + ~Context(); + + base::WeakPtr<Context> AsWeakPtr(); + + void Orphan(); + + void Begin(HWND parent_window, + const string16& url_protocol, + const string16& program_name, + const OpenWithDialogController::SetDefaultCallback& callback); + + private: + enum State { + // The Context has been constructed. + CONTEXT_INITIALIZED, + // The UI automation event handler is ready. + CONTEXT_AUTOMATION_READY, + // The automation results came back before the call to SHOpenWithDialog. + CONTEXT_WAITING_FOR_DIALOG, + // The call to SHOpenWithDialog returned before automation results. + CONTEXT_WAITING_FOR_RESULTS, + CONTEXT_FINISHED, + }; + + // Invokes the client's callback and destroys this instance. + void NotifyClientAndDie(); + + void OnTimeout(); + void OnInitialized(HRESULT result); + void OnAutomationResult(HRESULT result, std::vector<string16> choices); + void OnOpenWithComplete(HRESULT result); + + base::ThreadChecker thread_checker_; + State state_; + internal::UIAutomationClient automation_client_; + HWND parent_window_; + string16 file_name_; + string16 file_type_class_; + int open_as_info_flags_; + OpenWithDialogController::SetDefaultCallback callback_; + HRESULT open_with_result_; + HRESULT automation_result_; + std::vector<string16> automation_choices_; + base::WeakPtrFactory<Context> weak_ptr_factory_; + DISALLOW_COPY_AND_ASSIGN(OpenWithDialogController::Context); +}; + +OpenWithDialogController::Context::Context() + : state_(CONTEXT_INITIALIZED), + parent_window_(), + open_as_info_flags_(), + open_with_result_(E_FAIL), + automation_result_(E_FAIL), + ALLOW_THIS_IN_INITIALIZER_LIST(weak_ptr_factory_(this)) {} + +OpenWithDialogController::Context::~Context() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +base::WeakPtr<OpenWithDialogController::Context> + OpenWithDialogController::Context::AsWeakPtr() { + DCHECK(thread_checker_.CalledOnValidThread()); + return weak_ptr_factory_.GetWeakPtr(); +} + +void OpenWithDialogController::Context::Orphan() { + DCHECK(thread_checker_.CalledOnValidThread()); + + // The controller is being destroyed. Its client is no longer interested in + // having the interaction continue. + DLOG_IF(WARNING, (state_ == CONTEXT_AUTOMATION_READY || + state_ == CONTEXT_WAITING_FOR_DIALOG)) + << "Abandoning the OpenWithDialog."; + delete this; +} + +void OpenWithDialogController::Context::Begin( + HWND parent_window, + const string16& url_protocol, + const string16& program_name, + const OpenWithDialogController::SetDefaultCallback& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + + parent_window_ = parent_window; + file_name_ = url_protocol; + file_type_class_.clear(); + open_as_info_flags_ = (OAIF_URL_PROTOCOL | OAIF_FORCE_REGISTRATION | + OAIF_REGISTER_EXT); + callback_ = callback; + + // Post a delayed callback to abort the operation if it takes too long. + base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( + FROM_HERE, + base::Bind(&OpenWithDialogController::Context::OnTimeout, AsWeakPtr()), + base::TimeDelta::FromSeconds(kControllerTimeoutSeconds)); + + automation_client_.Begin( + kShellFlyoutClassName, + program_name, + base::Bind(&OpenWithDialogController::Context::OnInitialized, + AsWeakPtr()), + base::Bind(&OpenWithDialogController::Context::OnAutomationResult, + AsWeakPtr())); +} + +void OpenWithDialogController::Context::NotifyClientAndDie() { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK_EQ(state_, CONTEXT_FINISHED); + DLOG_IF(WARNING, SUCCEEDED(automation_result_) && FAILED(open_with_result_)) + << "Automation succeeded, yet SHOpenWithDialog failed."; + + // Ignore any future callbacks (such as the timeout) or calls to Orphan. + weak_ptr_factory_.InvalidateWeakPtrs(); + callback_.Run(automation_result_, automation_choices_); + delete this; +} + +void OpenWithDialogController::Context::OnTimeout() { + DCHECK(thread_checker_.CalledOnValidThread()); + // This is a LOG rather than a DLOG since it represents something that needs + // to be investigated and fixed. + LOG(ERROR) << __FUNCTION__ " state: " << state_; + + state_ = CONTEXT_FINISHED; + NotifyClientAndDie(); +} + +void OpenWithDialogController::Context::OnInitialized(HRESULT result) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK_EQ(state_, CONTEXT_INITIALIZED); + if (FAILED(result)) { + automation_result_ = result; + state_ = CONTEXT_FINISHED; + NotifyClientAndDie(); + return; + } + state_ = CONTEXT_AUTOMATION_READY; + OpenWithDialogAsync( + parent_window_, file_name_, file_type_class_, open_as_info_flags_, + base::Bind(&OpenWithDialogController::Context::OnOpenWithComplete, + weak_ptr_factory_.GetWeakPtr())); +} + +void OpenWithDialogController::Context::OnAutomationResult( + HRESULT result, + std::vector<string16> choices) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK_EQ(automation_result_, E_FAIL); + + automation_result_ = result; + automation_choices_ = choices; + switch (state_) { + case CONTEXT_AUTOMATION_READY: + // The results of automation are in and we're waiting for + // SHOpenWithDialog to return. + state_ = CONTEXT_WAITING_FOR_DIALOG; + break; + case CONTEXT_WAITING_FOR_RESULTS: + state_ = CONTEXT_FINISHED; + NotifyClientAndDie(); + break; + default: + NOTREACHED() << state_; + } +} + +void OpenWithDialogController::Context::OnOpenWithComplete(HRESULT result) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK_EQ(open_with_result_, E_FAIL); + + open_with_result_ = result; + switch (state_) { + case CONTEXT_AUTOMATION_READY: + // The interaction completed and we're waiting for the results from the + // automation side to come in. + state_ = CONTEXT_WAITING_FOR_RESULTS; + break; + case CONTEXT_WAITING_FOR_DIALOG: + // All results are in. Invoke the caller's callback. + state_ = CONTEXT_FINISHED; + NotifyClientAndDie(); + break; + default: + NOTREACHED() << state_; + } +} + +OpenWithDialogController::OpenWithDialogController() {} + +OpenWithDialogController::~OpenWithDialogController() { + // Orphan the context if this instance is being destroyed before the context + // finishes its work. + if (context_) + context_->Orphan(); +} + +void OpenWithDialogController::Begin( + HWND parent_window, + const string16& url_protocol, + const string16& program, + const SetDefaultCallback& callback) { + DCHECK_EQ(context_, static_cast<Context*>(NULL)); + if (base::win::GetVersion() < base::win::VERSION_WIN8) { + NOTREACHED() << "Windows 8 is required."; + // The callback may not properly handle being run from Begin, so post a task + // to this thread's task runner to call it. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::Bind(callback, E_FAIL, std::vector<string16>())); + return; + } + + context_ = (new Context())->AsWeakPtr(); + context_->Begin(parent_window, url_protocol, program, callback); +} + +HRESULT OpenWithDialogController::RunSynchronously( + HWND parent_window, + const string16& protocol, + const string16& program, + std::vector<string16>* choices) { + DCHECK_EQ(MessageLoop::current(), static_cast<MessageLoop*>(NULL)); + if (base::win::GetVersion() < base::win::VERSION_WIN8) { + NOTREACHED() << "Windows 8 is required."; + return E_FAIL; + } + + HRESULT result = S_OK; + MessageLoop message_loop; + base::RunLoop run_loop; + + message_loop.PostTask( + FROM_HERE, + base::Bind(&OpenWithDialogController::Begin, base::Unretained(this), + parent_window, protocol, program, + Bind(&OnMakeDefaultComplete, run_loop.QuitClosure(), + &result, choices))); + + run_loop.Run(); + return result; +} + +} // namespace win8 diff --git a/win8/test/open_with_dialog_controller.h b/win8/test/open_with_dialog_controller.h new file mode 100644 index 0000000..8c664fc --- /dev/null +++ b/win8/test/open_with_dialog_controller.h @@ -0,0 +1,61 @@ +// 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. + +#ifndef WIN8_TEST_OPEN_WITH_DIALOG_CONTROLLER_H_ +#define WIN8_TEST_OPEN_WITH_DIALOG_CONTROLLER_H_ + +#include <windows.h> + +#include <vector> + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/memory/weak_ptr.h" +#include "base/string16.h" + +namespace win8 { + +// Asynchronously drives Windows 8's OpenWithDialog into making a given program +// the default handler for an URL protocol. +class OpenWithDialogController { + public: + // A callback that is invoked upon completion of the OpenWithDialog + // interaction. If the HRESULT indicates success, the interaction completed + // successfully. Otherwise, the vector of strings may contain the list of + // possible choices if the desired program could not be selected. + typedef base::Callback<void(HRESULT, + std::vector<string16>)> SetDefaultCallback; + + OpenWithDialogController(); + ~OpenWithDialogController(); + + // Starts the process of making |program| the default handler for + // |url_protocol|. |parent_window| may be NULL. |callback| will be invoked + // upon completion. This instance may be deleted prior to |callback| being + // invoked to cancel the operation. + void Begin(HWND parent_window, + const string16& url_protocol, + const string16& program, + const SetDefaultCallback& callback); + + // Sychronously drives the dialog by running a message loop. Do not by any + // means call this on a thread that already has a message loop. Returns S_OK + // on success. Otherwise, |choices| may contain the list of possible choices + // if the desired program could not be selected. + HRESULT RunSynchronously(HWND parent_window, + const string16& url_protocol, + const string16& program, + std::vector<string16>* choices); + + private: + class Context; + + base::WeakPtr<Context> context_; + + DISALLOW_COPY_AND_ASSIGN(OpenWithDialogController); +}; + +} // namespace win8 + +#endif // WIN8_TEST_OPEN_WITH_DIALOG_CONTROLLER_H_ diff --git a/win8/test/ui_automation_client.cc b/win8/test/ui_automation_client.cc new file mode 100644 index 0000000..42de5d5 --- /dev/null +++ b/win8/test/ui_automation_client.cc @@ -0,0 +1,632 @@ +// 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 <uiautomation.h> + +#include <algorithm> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/memory/ref_counted.h" +#include "base/string_number_conversions.h" +#include "base/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, + string16 class_name, + 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<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. + string16 class_name_; + + // The name of the item to invoke. + 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); + + 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(); + base::WeakPtr<Context> context_ptr(context->weak_ptr_factory_.GetWeakPtr()); + // Unbind from this thread so that the instance will bind to the automation + // thread when Initialize is called. + context->weak_ptr_factory_.DetachFromThread(); + return context_ptr; +} + +void UIAutomationClient::Context::DeleteOnAutomationThread() { + DCHECK(thread_checker_.CalledOnValidThread()); + delete this; +} + +UIAutomationClient::Context::Context() + : ALLOW_THIS_IN_INITIALIZER_LIST(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, + string16 class_name, + 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, + TreeScope_Descendants, + cache_request, + event_handler); + + 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, + event_handler_); + 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) != VT_BSTR) { + LOG(ERROR) << __FUNCTION__ " class name is not a BSTR: " << V_VT(&var); + return; + } + + string16 class_name(V_BSTR(&var)); + + // Window class names are atoms, which are case-insensitive. + if (class_name.size() == class_name_.size() && + std::equal(class_name.begin(), class_name.end(), class_name_.begin(), + base::CaseInsensitiveCompare<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<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 false; + } + + 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, + cache_request, + target.Receive()); + if (FAILED(result)) { + LOG(ERROR) << std::hex << result; + return result; + } + + // If the item was found, invoke it. + if (!target.get()) { + LOG(ERROR) << "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<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, control_view_condition, 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, + cache_request, + 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) != VT_BSTR) { + LOG(ERROR) << __FUNCTION__ " name is not a BSTR: " << V_VT(&var); + continue; + } + choices->push_back(string16(V_BSTR(&var))); + 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) != VT_I4) { + LOG(ERROR) << __FUNCTION__ " window handle is not an int: " << V_VT(&var); + return; + } + + HWND handle = reinterpret_cast<HWND>(V_I4(&var)); + + uint32 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 string16& item_name, + const InitializedCallback& init_callback, + const ResultCallback& result_callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + DCHECK_EQ(context_, 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(), + string16(class_name), + item_name, + init_callback, + result_callback)); +} + +} // namespace internal +} // namespace win8 diff --git a/win8/test/ui_automation_client.h b/win8/test/ui_automation_client.h new file mode 100644 index 0000000..c285c7b --- /dev/null +++ b/win8/test/ui_automation_client.h @@ -0,0 +1,70 @@ +// 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. + +#ifndef WIN8_TEST_UI_AUTOMATION_CLIENT_H_ +#define WIN8_TEST_UI_AUTOMATION_CLIENT_H_ + +// This file contains UI automation implementation details for the +// OpenWithDialogController. See that class for consumable entrypoints. + +#include <windows.h> +#include <vector> + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/memory/weak_ptr.h" +#include "base/string16.h" +#include "base/threading/thread.h" +#include "base/threading/thread_checker.h" + +namespace win8 { +namespace internal { + +// An asynchronous UI automation client that waits for the appearance of a +// Window of a particular class and then invokes one of its children identified +// by name. An instance may be destroyed at any time after Begin() is invoked. +// Any pending operation is cancelled. +class UIAutomationClient { + public: + // If the HRESULT argument indicates success, the automation client has been + // initialized, has installed its Window observer, and is ready to process + // the named window. Otherwise, initialization has failed and ResultCallback + // will not be invoked. + typedef base::Callback<void(HRESULT)> InitializedCallback; + + // If the HRESULT argument indicates success, the desired item in the window + // was invoked. Otherwise, the string vector (if not empty) contains the list + // of possible items in the window. + typedef base::Callback<void(HRESULT, std::vector<string16>)> ResultCallback; + + UIAutomationClient(); + ~UIAutomationClient(); + + // Starts the client. Invokes |callback| once the client is ready or upon + // failure to start, in which case |result_callback| will never be called. + // Otherwise, |result_callback| will be invoked once |item_name| has been + // invoked. + void Begin(const wchar_t* class_name, + const string16& item_name, + const InitializedCallback& init_callback, + const ResultCallback& result_callback); + + private: + class Context; + + base::ThreadChecker thread_checker_; + + // A thread in the COM MTA in which automation calls are made. + base::Thread automation_thread_; + + // A pointer to the context object that lives on the automation thread. + base::WeakPtr<Context> context_; + + DISALLOW_COPY_AND_ASSIGN(UIAutomationClient); +}; + +} // namespace internal +} // namespace win8 + +#endif // WIN8_TEST_UI_AUTOMATION_CLIENT_H_ diff --git a/win8/win8.gyp b/win8/win8.gyp index d6ca61f..5790940 100644 --- a/win8/win8.gyp +++ b/win8/win8.gyp @@ -46,5 +46,20 @@ 'util/win8_util.h', ], }, + { + 'target_name': 'test_support_win8', + 'type': 'static_library', + 'dependencies': [ + '../base/base.gyp:base', + ], + 'sources': [ + 'test/open_with_dialog_async.cc', + 'test/open_with_dialog_async.h', + 'test/open_with_dialog_controller.cc', + 'test/open_with_dialog_controller.h', + 'test/ui_automation_client.cc', + 'test/ui_automation_client.h', + ], + }, ], } |