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