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