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