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 "apps/shell_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_system.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_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 39 using apps::ShellWindow; 40 using content::BrowserThread; 41 42 namespace { 43 44 const int kFileManagerWidth = 972; // pixels 45 const int kFileManagerHeight = 640; // pixels 46 const int kFileManagerMinimumWidth = 320; // pixels 47 const int kFileManagerMinimumHeight = 240; // pixels 48 49 // Holds references to file manager dialogs that have callbacks pending 50 // to their listeners. 51 class 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 68 PendingDialog* PendingDialog::GetInstance() { 69 return Singleton<PendingDialog>::get(); 70 } 71 72 void 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 81 void PendingDialog::Remove(SelectFileDialogExtension::RoutingID id) { 82 map_.erase(id); 83 } 84 85 scoped_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 98 SelectFileDialogExtension::RoutingID 99 SelectFileDialogExtension::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 109 SelectFileDialogExtension* SelectFileDialogExtension::Create( 110 Listener* listener, 111 ui::SelectFilePolicy* policy) { 112 return new SelectFileDialogExtension(listener, policy); 113 } 114 115 SelectFileDialogExtension::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 128 SelectFileDialogExtension::~SelectFileDialogExtension() { 129 if (extension_dialog_.get()) 130 extension_dialog_->ObserverDestroyed(); 131 } 132 133 bool SelectFileDialogExtension::IsRunning( 134 gfx::NativeWindow owner_window) const { 135 return owner_window_ == owner_window; 136 } 137 138 void SelectFileDialogExtension::ListenerDestroyed() { 139 listener_ = NULL; 140 params_ = NULL; 141 PendingDialog::GetInstance()->Remove(routing_id_); 142 } 143 144 void 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 155 void 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 183 void 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 198 void 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 211 void 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 221 content::RenderViewHost* SelectFileDialogExtension::GetRenderViewHost() { 222 if (extension_dialog_.get()) 223 return extension_dialog_->host()->render_view_host(); 224 return NULL; 225 } 226 227 void 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 248 void SelectFileDialogExtension::AddPending(RoutingID routing_id) { 249 PendingDialog::GetInstance()->Add(routing_id, this); 250 } 251 252 // static 253 bool SelectFileDialogExtension::PendingExists(RoutingID routing_id) { 254 return PendingDialog::GetInstance()->Find(routing_id).get() != NULL; 255 } 256 257 bool SelectFileDialogExtension::HasMultipleFileTypeChoicesImpl() { 258 return has_multiple_file_type_choices_; 259 } 260 261 void 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 ShellWindow is 282 // needed. 283 Browser* owner_browser = NULL; 284 ShellWindow* shell_window = NULL; 285 286 // If owner_window is supplied, use that to find a browser or a shell 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 shell window. 292 shell_window = apps::ShellWindowRegistry:: 293 GetShellWindowForNativeWindowAnyProfile(owner_window); 294 } 295 } 296 297 if (shell_window) { 298 if (shell_window->window_type_is_panel()) { 299 NOTREACHED() << "File dialog opened by panel."; 300 return; 301 } 302 base_window = shell_window->GetBaseWindow(); 303 web_contents = shell_window->web_contents(); 304 } else { 305 // If the owning window is still unknown, this could be a background page or 306 // and extension popup. Use the last active browser. 307 if (!owner_browser) { 308 owner_browser = 309 chrome::FindLastActiveWithHostDesktopType(chrome::GetActiveDesktop()); 310 } 311 DCHECK(owner_browser); 312 if (!owner_browser) { 313 LOG(ERROR) << "Could not find browser or shell window for popup."; 314 return; 315 } 316 base_window = owner_browser->window(); 317 web_contents = owner_browser->tab_strip_model()->GetActiveWebContents(); 318 } 319 320 DCHECK(base_window); 321 DCHECK(web_contents); 322 profile_ = Profile::FromBrowserContext(web_contents->GetBrowserContext()); 323 DCHECK(profile_); 324 325 // Check if we have another dialog opened for the contents. It's unlikely, but 326 // possible. 327 RoutingID routing_id = GetRoutingIDFromWebContents(web_contents); 328 if (PendingExists(routing_id)) { 329 DLOG(WARNING) << "Pending dialog exists with id " << routing_id; 330 return; 331 } 332 333 base::FilePath default_dialog_path; 334 335 const PrefService* pref_service = profile_->GetPrefs(); 336 337 if (default_path.empty() && pref_service) { 338 default_dialog_path = 339 pref_service->GetFilePath(prefs::kDownloadDefaultDirectory); 340 } else { 341 default_dialog_path = default_path; 342 } 343 344 base::FilePath virtual_path; 345 base::FilePath fallback_path = profile_->last_selected_directory().Append( 346 default_dialog_path.BaseName()); 347 // If an absolute path is specified as the default path, convert it to the 348 // virtual path in the file browser extension. Due to the current design, 349 // an invalid temporal cache file path may passed as |default_dialog_path| 350 // (crbug.com/178013 #9-#11). In such a case, we use the last selected 351 // directory as a workaround. Real fix is tracked at crbug.com/110119. 352 using file_manager::kFileManagerAppId; 353 if (default_dialog_path.IsAbsolute() && 354 (file_manager::util::ConvertAbsoluteFilePathToRelativeFileSystemPath( 355 profile_, kFileManagerAppId, default_dialog_path, &virtual_path) || 356 file_manager::util::ConvertAbsoluteFilePathToRelativeFileSystemPath( 357 profile_, kFileManagerAppId, fallback_path, &virtual_path))) { 358 virtual_path = base::FilePath("/").Append(virtual_path); 359 } else { 360 // If the path was relative, or failed to convert, just use the base name, 361 virtual_path = default_dialog_path.BaseName(); 362 } 363 364 has_multiple_file_type_choices_ = 365 file_types ? file_types->extensions.size() > 1 : true; 366 367 GURL file_manager_url = 368 file_manager::util::GetFileManagerMainPageUrlWithParams( 369 type, title, virtual_path, file_types, file_type_index, 370 default_extension); 371 372 ExtensionDialog* dialog = ExtensionDialog::Show(file_manager_url, 373 base_window, profile_, web_contents, 374 kFileManagerWidth, kFileManagerHeight, 375 kFileManagerMinimumWidth, 376 kFileManagerMinimumHeight, 377 #if defined(USE_AURA) 378 file_manager::util::GetSelectFileDialogTitle(type), 379 #else 380 // HTML-based header used. 381 base::string16(), 382 #endif 383 this /* ExtensionDialog::Observer */); 384 if (!dialog) { 385 LOG(ERROR) << "Unable to create extension dialog"; 386 return; 387 } 388 389 // Connect our listener to FileDialogFunction's per-tab callbacks. 390 AddPending(routing_id); 391 392 extension_dialog_ = dialog; 393 params_ = params; 394 routing_id_ = routing_id; 395 owner_window_ = owner_window; 396 } 397