select_file_dialog_extension.cc revision f2477e01787aa58f445919b809d89e252beef54f
1// Copyright (c) 2012 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 "chrome/browser/ui/views/select_file_dialog_extension.h"
6
7#include "apps/shell_window.h"
8#include "apps/shell_window_registry.h"
9#include "apps/ui/native_app_window.h"
10#include "base/bind.h"
11#include "base/callback.h"
12#include "base/logging.h"
13#include "base/memory/ref_counted.h"
14#include "base/memory/singleton.h"
15#include "base/message_loop/message_loop.h"
16#include "chrome/browser/chromeos/file_manager/app_id.h"
17#include "chrome/browser/chromeos/file_manager/fileapi_util.h"
18#include "chrome/browser/chromeos/file_manager/select_file_dialog_util.h"
19#include "chrome/browser/chromeos/file_manager/url_util.h"
20#include "chrome/browser/extensions/extension_service.h"
21#include "chrome/browser/extensions/extension_system.h"
22#include "chrome/browser/extensions/extension_view_host.h"
23#include "chrome/browser/profiles/profile.h"
24#include "chrome/browser/sessions/session_tab_helper.h"
25#include "chrome/browser/ui/browser.h"
26#include "chrome/browser/ui/browser_finder.h"
27#include "chrome/browser/ui/browser_list.h"
28#include "chrome/browser/ui/browser_window.h"
29#include "chrome/browser/ui/chrome_select_file_policy.h"
30#include "chrome/browser/ui/host_desktop.h"
31#include "chrome/browser/ui/tabs/tab_strip_model.h"
32#include "chrome/browser/ui/views/extensions/extension_dialog.h"
33#include "chrome/common/pref_names.h"
34#include "content/public/browser/browser_thread.h"
35#include "ui/base/base_window.h"
36#include "ui/shell_dialogs/selected_file_info.h"
37#include "ui/views/widget/widget.h"
38
39using apps::ShellWindow;
40using content::BrowserThread;
41
42namespace {
43
44const int kFileManagerWidth = 972;  // pixels
45const int kFileManagerHeight = 640;  // pixels
46
47// Holds references to file manager dialogs that have callbacks pending
48// to their listeners.
49class PendingDialog {
50 public:
51  static PendingDialog* GetInstance();
52  void Add(SelectFileDialogExtension::RoutingID id,
53           scoped_refptr<SelectFileDialogExtension> dialog);
54  void Remove(SelectFileDialogExtension::RoutingID id);
55  scoped_refptr<SelectFileDialogExtension> Find(
56      SelectFileDialogExtension::RoutingID id);
57
58 private:
59  friend struct DefaultSingletonTraits<PendingDialog>;
60  typedef std::map<SelectFileDialogExtension::RoutingID,
61                   scoped_refptr<SelectFileDialogExtension> > Map;
62  Map map_;
63};
64
65// static
66PendingDialog* PendingDialog::GetInstance() {
67  return Singleton<PendingDialog>::get();
68}
69
70void PendingDialog::Add(SelectFileDialogExtension::RoutingID id,
71                        scoped_refptr<SelectFileDialogExtension> dialog) {
72  DCHECK(dialog.get());
73  if (map_.find(id) == map_.end())
74    map_.insert(std::make_pair(id, dialog));
75  else
76    DLOG(WARNING) << "Duplicate pending dialog " << id;
77}
78
79void PendingDialog::Remove(SelectFileDialogExtension::RoutingID id) {
80  map_.erase(id);
81}
82
83scoped_refptr<SelectFileDialogExtension> PendingDialog::Find(
84    SelectFileDialogExtension::RoutingID id) {
85  Map::const_iterator it = map_.find(id);
86  if (it == map_.end())
87    return NULL;
88  return it->second;
89}
90
91}  // namespace
92
93/////////////////////////////////////////////////////////////////////////////
94
95// static
96SelectFileDialogExtension::RoutingID
97SelectFileDialogExtension::GetRoutingIDFromWebContents(
98    const content::WebContents* web_contents) {
99  // Use the raw pointer value as the identifier. Previously we have used the
100  // tab ID for the purpose, but some web_contents, especially those of the
101  // packaged apps, don't have tab IDs assigned.
102  return web_contents;
103}
104
105// TODO(jamescook): Move this into a new file shell_dialogs_chromeos.cc
106// static
107SelectFileDialogExtension* SelectFileDialogExtension::Create(
108    Listener* listener,
109    ui::SelectFilePolicy* policy) {
110  return new SelectFileDialogExtension(listener, policy);
111}
112
113SelectFileDialogExtension::SelectFileDialogExtension(
114    Listener* listener,
115    ui::SelectFilePolicy* policy)
116    : SelectFileDialog(listener, policy),
117      has_multiple_file_type_choices_(false),
118      routing_id_(),
119      profile_(NULL),
120      owner_window_(NULL),
121      selection_type_(CANCEL),
122      selection_index_(0),
123      params_(NULL) {
124}
125
126SelectFileDialogExtension::~SelectFileDialogExtension() {
127  if (extension_dialog_.get())
128    extension_dialog_->ObserverDestroyed();
129}
130
131bool SelectFileDialogExtension::IsRunning(
132    gfx::NativeWindow owner_window) const {
133  return owner_window_ == owner_window;
134}
135
136void SelectFileDialogExtension::ListenerDestroyed() {
137  listener_ = NULL;
138  params_ = NULL;
139  PendingDialog::GetInstance()->Remove(routing_id_);
140}
141
142void SelectFileDialogExtension::ExtensionDialogClosing(
143    ExtensionDialog* /*dialog*/) {
144  profile_ = NULL;
145  owner_window_ = NULL;
146  // Release our reference to the underlying dialog to allow it to close.
147  extension_dialog_ = NULL;
148  PendingDialog::GetInstance()->Remove(routing_id_);
149  // Actually invoke the appropriate callback on our listener.
150  NotifyListener();
151}
152
153void SelectFileDialogExtension::ExtensionTerminated(
154    ExtensionDialog* dialog) {
155  // The extension would have been unloaded because of the termination,
156  // reload it.
157  std::string extension_id = dialog->host()->extension()->id();
158  // Reload the extension after a bit; the extension may not have been unloaded
159  // yet. We don't want to try to reload the extension only to have the Unload
160  // code execute after us and re-unload the extension.
161  //
162  // TODO(rkc): This is ugly. The ideal solution is that we shouldn't need to
163  // reload the extension at all - when we try to open the extension the next
164  // time, the extension subsystem would automatically reload it for us. At
165  // this time though this is broken because of some faulty wiring in
166  // extensions::ProcessManager::CreateViewHost. Once that is fixed, remove
167  // this.
168  if (profile_) {
169    base::MessageLoop::current()->PostTask(
170        FROM_HERE,
171        base::Bind(&ExtensionService::ReloadExtension,
172                   base::Unretained(extensions::ExtensionSystem::Get(profile_)
173                                        ->extension_service()),
174                   extension_id));
175  }
176
177  dialog->GetWidget()->Close();
178}
179
180// static
181void SelectFileDialogExtension::OnFileSelected(
182    RoutingID routing_id,
183    const ui::SelectedFileInfo& file,
184    int index) {
185  scoped_refptr<SelectFileDialogExtension> dialog =
186      PendingDialog::GetInstance()->Find(routing_id);
187  if (!dialog.get())
188    return;
189  dialog->selection_type_ = SINGLE_FILE;
190  dialog->selection_files_.clear();
191  dialog->selection_files_.push_back(file);
192  dialog->selection_index_ = index;
193}
194
195// static
196void SelectFileDialogExtension::OnMultiFilesSelected(
197    RoutingID routing_id,
198    const std::vector<ui::SelectedFileInfo>& files) {
199  scoped_refptr<SelectFileDialogExtension> dialog =
200      PendingDialog::GetInstance()->Find(routing_id);
201  if (!dialog.get())
202    return;
203  dialog->selection_type_ = MULTIPLE_FILES;
204  dialog->selection_files_ = files;
205  dialog->selection_index_ = 0;
206}
207
208// static
209void SelectFileDialogExtension::OnFileSelectionCanceled(RoutingID routing_id) {
210  scoped_refptr<SelectFileDialogExtension> dialog =
211      PendingDialog::GetInstance()->Find(routing_id);
212  if (!dialog.get())
213    return;
214  dialog->selection_type_ = CANCEL;
215  dialog->selection_files_.clear();
216  dialog->selection_index_ = 0;
217}
218
219content::RenderViewHost* SelectFileDialogExtension::GetRenderViewHost() {
220  if (extension_dialog_.get())
221    return extension_dialog_->host()->render_view_host();
222  return NULL;
223}
224
225void SelectFileDialogExtension::NotifyListener() {
226  if (!listener_)
227    return;
228  switch (selection_type_) {
229    case CANCEL:
230      listener_->FileSelectionCanceled(params_);
231      break;
232    case SINGLE_FILE:
233      listener_->FileSelectedWithExtraInfo(selection_files_[0],
234                                           selection_index_,
235                                           params_);
236      break;
237    case MULTIPLE_FILES:
238      listener_->MultiFilesSelectedWithExtraInfo(selection_files_, params_);
239      break;
240    default:
241      NOTREACHED();
242      break;
243  }
244}
245
246void SelectFileDialogExtension::AddPending(RoutingID routing_id) {
247  PendingDialog::GetInstance()->Add(routing_id, this);
248}
249
250// static
251bool SelectFileDialogExtension::PendingExists(RoutingID routing_id) {
252  return PendingDialog::GetInstance()->Find(routing_id).get() != NULL;
253}
254
255bool SelectFileDialogExtension::HasMultipleFileTypeChoicesImpl() {
256  return has_multiple_file_type_choices_;
257}
258
259void SelectFileDialogExtension::SelectFileImpl(
260    Type type,
261    const string16& title,
262    const base::FilePath& default_path,
263    const FileTypeInfo* file_types,
264    int file_type_index,
265    const base::FilePath::StringType& default_extension,
266    gfx::NativeWindow owner_window,
267    void* params) {
268  if (owner_window_) {
269    LOG(ERROR) << "File dialog already in use!";
270    return;
271  }
272
273  // The base window to associate the dialog with.
274  ui::BaseWindow* base_window = NULL;
275
276  // The web contents to associate the dialog with.
277  content::WebContents* web_contents = NULL;
278
279  // To get the base_window and profile, either a Browser or ShellWindow is
280  // needed.
281  Browser* owner_browser =  NULL;
282  ShellWindow* shell_window = NULL;
283
284  // If owner_window is supplied, use that to find a browser or a shell window.
285  if (owner_window) {
286    owner_browser = chrome::FindBrowserWithWindow(owner_window);
287    if (!owner_browser) {
288      // If an owner_window was supplied but we couldn't find a browser, this
289      // could be for a shell window.
290      shell_window = apps::ShellWindowRegistry::
291          GetShellWindowForNativeWindowAnyProfile(owner_window);
292    }
293  }
294
295  if (shell_window) {
296    if (shell_window->window_type_is_panel()) {
297      NOTREACHED() << "File dialog opened by panel.";
298      return;
299    }
300    base_window = shell_window->GetBaseWindow();
301    web_contents = shell_window->web_contents();
302  } else {
303    // If the owning window is still unknown, this could be a background page or
304    // and extension popup. Use the last active browser.
305    if (!owner_browser) {
306      owner_browser =
307          chrome::FindLastActiveWithHostDesktopType(chrome::GetActiveDesktop());
308    }
309    DCHECK(owner_browser);
310    if (!owner_browser) {
311      LOG(ERROR) << "Could not find browser or shell window for popup.";
312      return;
313    }
314    base_window = owner_browser->window();
315    web_contents = owner_browser->tab_strip_model()->GetActiveWebContents();
316  }
317
318  DCHECK(base_window);
319  DCHECK(web_contents);
320  profile_ = Profile::FromBrowserContext(web_contents->GetBrowserContext());
321  DCHECK(profile_);
322
323  // Check if we have another dialog opened for the contents. It's unlikely, but
324  // possible.
325  RoutingID routing_id = GetRoutingIDFromWebContents(web_contents);
326  if (PendingExists(routing_id)) {
327    DLOG(WARNING) << "Pending dialog exists with id " << routing_id;
328    return;
329  }
330
331  base::FilePath default_dialog_path;
332
333  const PrefService* pref_service = profile_->GetPrefs();
334
335  if (default_path.empty() && pref_service) {
336    default_dialog_path =
337        pref_service->GetFilePath(prefs::kDownloadDefaultDirectory);
338  } else {
339    default_dialog_path = default_path;
340  }
341
342  base::FilePath virtual_path;
343  base::FilePath fallback_path = profile_->last_selected_directory().Append(
344      default_dialog_path.BaseName());
345  // If an absolute path is specified as the default path, convert it to the
346  // virtual path in the file browser extension. Due to the current design,
347  // an invalid temporal cache file path may passed as |default_dialog_path|
348  // (crbug.com/178013 #9-#11). In such a case, we use the last selected
349  // directory as a workaround. Real fix is tracked at crbug.com/110119.
350  using file_manager::kFileManagerAppId;
351  if (default_dialog_path.IsAbsolute() &&
352      (file_manager::util::ConvertAbsoluteFilePathToRelativeFileSystemPath(
353           profile_, kFileManagerAppId, default_dialog_path, &virtual_path) ||
354       file_manager::util::ConvertAbsoluteFilePathToRelativeFileSystemPath(
355           profile_, kFileManagerAppId, fallback_path, &virtual_path))) {
356    virtual_path = base::FilePath("/").Append(virtual_path);
357  } else {
358    // If the path was relative, or failed to convert, just use the base name,
359    virtual_path = default_dialog_path.BaseName();
360  }
361
362  has_multiple_file_type_choices_ =
363      file_types ? file_types->extensions.size() > 1 : true;
364
365  GURL file_manager_url =
366      file_manager::util::GetFileManagerMainPageUrlWithParams(
367          type, title, virtual_path, file_types, file_type_index,
368          default_extension);
369
370  ExtensionDialog* dialog = ExtensionDialog::Show(file_manager_url,
371      base_window, profile_, web_contents,
372      kFileManagerWidth, kFileManagerHeight,
373      kFileManagerWidth,
374      kFileManagerHeight,
375#if defined(USE_AURA)
376      file_manager::util::GetSelectFileDialogTitle(type),
377#else
378      // HTML-based header used.
379      string16(),
380#endif
381      this /* ExtensionDialog::Observer */);
382  if (!dialog) {
383    LOG(ERROR) << "Unable to create extension dialog";
384    return;
385  }
386
387  // Connect our listener to FileDialogFunction's per-tab callbacks.
388  AddPending(routing_id);
389
390  extension_dialog_ = dialog;
391  params_ = params;
392  routing_id_ = routing_id;
393  owner_window_ = owner_window;
394}
395