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