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