1// Copyright (c) 2013 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.
4
5#include "win8/test/open_with_dialog_controller.h"
6
7#include <shlobj.h>
8
9#include "base/bind.h"
10#include "base/callback.h"
11#include "base/logging.h"
12#include "base/memory/scoped_ptr.h"
13#include "base/message_loop/message_loop.h"
14#include "base/run_loop.h"
15#include "base/thread_task_runner_handle.h"
16#include "base/threading/thread_checker.h"
17#include "base/win/windows_version.h"
18#include "win8/test/open_with_dialog_async.h"
19#include "win8/test/ui_automation_client.h"
20
21namespace win8 {
22
23namespace {
24
25const int kControllerTimeoutSeconds = 5;
26const wchar_t kShellFlyoutClassName[] = L"Shell_Flyout";
27
28// A callback invoked with the OpenWithDialogController's results. Said results
29// are copied to |result_out| and |choices_out| and then |closure| is invoked.
30// This function is in support of OpenWithDialogController::RunSynchronously.
31void OnMakeDefaultComplete(
32    const base::Closure& closure,
33    HRESULT* result_out,
34    std::vector<string16>* choices_out,
35    HRESULT hr,
36    std::vector<string16> choices) {
37  *result_out = hr;
38  *choices_out = choices;
39  closure.Run();
40}
41
42}  // namespace
43
44// Lives on the main thread and is owned by a controller. May outlive the
45// controller (see Orphan).
46class OpenWithDialogController::Context {
47 public:
48  Context();
49  ~Context();
50
51  base::WeakPtr<Context> AsWeakPtr();
52
53  void Orphan();
54
55  void Begin(HWND parent_window,
56             const string16& url_protocol,
57             const string16& program_name,
58             const OpenWithDialogController::SetDefaultCallback& callback);
59
60 private:
61  enum State {
62    // The Context has been constructed.
63    CONTEXT_INITIALIZED,
64    // The UI automation event handler is ready.
65    CONTEXT_AUTOMATION_READY,
66    // The automation results came back before the call to SHOpenWithDialog.
67    CONTEXT_WAITING_FOR_DIALOG,
68    // The call to SHOpenWithDialog returned before automation results.
69    CONTEXT_WAITING_FOR_RESULTS,
70    CONTEXT_FINISHED,
71  };
72
73  // Invokes the client's callback and destroys this instance.
74  void NotifyClientAndDie();
75
76  void OnTimeout();
77  void OnInitialized(HRESULT result);
78  void OnAutomationResult(HRESULT result, std::vector<string16> choices);
79  void OnOpenWithComplete(HRESULT result);
80
81  base::ThreadChecker thread_checker_;
82  State state_;
83  internal::UIAutomationClient automation_client_;
84  HWND parent_window_;
85  string16 file_name_;
86  string16 file_type_class_;
87  int open_as_info_flags_;
88  OpenWithDialogController::SetDefaultCallback callback_;
89  HRESULT open_with_result_;
90  HRESULT automation_result_;
91  std::vector<string16> automation_choices_;
92  base::WeakPtrFactory<Context> weak_ptr_factory_;
93  DISALLOW_COPY_AND_ASSIGN(OpenWithDialogController::Context);
94};
95
96OpenWithDialogController::Context::Context()
97    : state_(CONTEXT_INITIALIZED),
98      parent_window_(),
99      open_as_info_flags_(),
100      open_with_result_(E_FAIL),
101      automation_result_(E_FAIL),
102      weak_ptr_factory_(this) {}
103
104OpenWithDialogController::Context::~Context() {
105  DCHECK(thread_checker_.CalledOnValidThread());
106}
107
108base::WeakPtr<OpenWithDialogController::Context>
109    OpenWithDialogController::Context::AsWeakPtr() {
110  DCHECK(thread_checker_.CalledOnValidThread());
111  return weak_ptr_factory_.GetWeakPtr();
112}
113
114void OpenWithDialogController::Context::Orphan() {
115  DCHECK(thread_checker_.CalledOnValidThread());
116
117  // The controller is being destroyed. Its client is no longer interested in
118  // having the interaction continue.
119  DLOG_IF(WARNING, (state_ == CONTEXT_AUTOMATION_READY ||
120                    state_ == CONTEXT_WAITING_FOR_DIALOG))
121      << "Abandoning the OpenWithDialog.";
122  delete this;
123}
124
125void OpenWithDialogController::Context::Begin(
126    HWND parent_window,
127    const string16& url_protocol,
128    const string16& program_name,
129    const OpenWithDialogController::SetDefaultCallback& callback) {
130  DCHECK(thread_checker_.CalledOnValidThread());
131
132  parent_window_ = parent_window;
133  file_name_ = url_protocol;
134  file_type_class_.clear();
135  open_as_info_flags_ = (OAIF_URL_PROTOCOL | OAIF_FORCE_REGISTRATION |
136                         OAIF_REGISTER_EXT);
137  callback_ = callback;
138
139  // Post a delayed callback to abort the operation if it takes too long.
140  base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
141      FROM_HERE,
142      base::Bind(&OpenWithDialogController::Context::OnTimeout, AsWeakPtr()),
143      base::TimeDelta::FromSeconds(kControllerTimeoutSeconds));
144
145  automation_client_.Begin(
146      kShellFlyoutClassName,
147      program_name,
148      base::Bind(&OpenWithDialogController::Context::OnInitialized,
149                 AsWeakPtr()),
150      base::Bind(&OpenWithDialogController::Context::OnAutomationResult,
151                 AsWeakPtr()));
152}
153
154void OpenWithDialogController::Context::NotifyClientAndDie() {
155  DCHECK(thread_checker_.CalledOnValidThread());
156  DCHECK_EQ(state_, CONTEXT_FINISHED);
157  DLOG_IF(WARNING, SUCCEEDED(automation_result_) && FAILED(open_with_result_))
158      << "Automation succeeded, yet SHOpenWithDialog failed.";
159
160  // Ignore any future callbacks (such as the timeout) or calls to Orphan.
161  weak_ptr_factory_.InvalidateWeakPtrs();
162  callback_.Run(automation_result_, automation_choices_);
163  delete this;
164}
165
166void OpenWithDialogController::Context::OnTimeout() {
167  DCHECK(thread_checker_.CalledOnValidThread());
168  // This is a LOG rather than a DLOG since it represents something that needs
169  // to be investigated and fixed.
170  LOG(ERROR) << __FUNCTION__ " state: " << state_;
171
172  state_ = CONTEXT_FINISHED;
173  NotifyClientAndDie();
174}
175
176void OpenWithDialogController::Context::OnInitialized(HRESULT result) {
177  DCHECK(thread_checker_.CalledOnValidThread());
178  DCHECK_EQ(state_, CONTEXT_INITIALIZED);
179  if (FAILED(result)) {
180    automation_result_ = result;
181    state_ = CONTEXT_FINISHED;
182    NotifyClientAndDie();
183    return;
184  }
185  state_ = CONTEXT_AUTOMATION_READY;
186  OpenWithDialogAsync(
187      parent_window_, file_name_, file_type_class_, open_as_info_flags_,
188      base::Bind(&OpenWithDialogController::Context::OnOpenWithComplete,
189                 weak_ptr_factory_.GetWeakPtr()));
190}
191
192void OpenWithDialogController::Context::OnAutomationResult(
193    HRESULT result,
194    std::vector<string16> choices) {
195  DCHECK(thread_checker_.CalledOnValidThread());
196  DCHECK_EQ(automation_result_, E_FAIL);
197
198  automation_result_ = result;
199  automation_choices_ = choices;
200  switch (state_) {
201    case CONTEXT_AUTOMATION_READY:
202      // The results of automation are in and we're waiting for
203      // SHOpenWithDialog to return.
204      state_ = CONTEXT_WAITING_FOR_DIALOG;
205      break;
206    case CONTEXT_WAITING_FOR_RESULTS:
207      state_ = CONTEXT_FINISHED;
208      NotifyClientAndDie();
209      break;
210    default:
211      NOTREACHED() << state_;
212  }
213}
214
215void OpenWithDialogController::Context::OnOpenWithComplete(HRESULT result) {
216  DCHECK(thread_checker_.CalledOnValidThread());
217  DCHECK_EQ(open_with_result_, E_FAIL);
218
219  open_with_result_ = result;
220  switch (state_) {
221    case CONTEXT_AUTOMATION_READY:
222      // The interaction completed and we're waiting for the results from the
223      // automation side to come in.
224      state_ = CONTEXT_WAITING_FOR_RESULTS;
225      break;
226    case CONTEXT_WAITING_FOR_DIALOG:
227      // All results are in.  Invoke the caller's callback.
228      state_ = CONTEXT_FINISHED;
229      NotifyClientAndDie();
230      break;
231    default:
232      NOTREACHED() << state_;
233  }
234}
235
236OpenWithDialogController::OpenWithDialogController() {}
237
238OpenWithDialogController::~OpenWithDialogController() {
239  // Orphan the context if this instance is being destroyed before the context
240  // finishes its work.
241  if (context_)
242    context_->Orphan();
243}
244
245void OpenWithDialogController::Begin(
246    HWND parent_window,
247    const string16& url_protocol,
248    const string16& program,
249    const SetDefaultCallback& callback) {
250  DCHECK_EQ(context_.get(), static_cast<Context*>(NULL));
251  if (base::win::GetVersion() < base::win::VERSION_WIN8) {
252    NOTREACHED() << "Windows 8 is required.";
253    // The callback may not properly handle being run from Begin, so post a task
254    // to this thread's task runner to call it.
255    base::ThreadTaskRunnerHandle::Get()->PostTask(
256        FROM_HERE,
257        base::Bind(callback, E_FAIL, std::vector<string16>()));
258    return;
259  }
260
261  context_ = (new Context())->AsWeakPtr();
262  context_->Begin(parent_window, url_protocol, program, callback);
263}
264
265HRESULT OpenWithDialogController::RunSynchronously(
266    HWND parent_window,
267    const string16& protocol,
268    const string16& program,
269    std::vector<string16>* choices) {
270  DCHECK_EQ(base::MessageLoop::current(),
271            static_cast<base::MessageLoop*>(NULL));
272  if (base::win::GetVersion() < base::win::VERSION_WIN8) {
273    NOTREACHED() << "Windows 8 is required.";
274    return E_FAIL;
275  }
276
277  HRESULT result = S_OK;
278  base::MessageLoop message_loop;
279  base::RunLoop run_loop;
280
281  message_loop.PostTask(
282      FROM_HERE,
283      base::Bind(&OpenWithDialogController::Begin, base::Unretained(this),
284                 parent_window, protocol, program,
285                 Bind(&OnMakeDefaultComplete, run_loop.QuitClosure(),
286                      &result, choices)));
287
288  run_loop.Run();
289  return result;
290}
291
292}  // namespace win8
293