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