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 45 using extensions::AppWindow; 46 using content::BrowserThread; 47 48 namespace { 49 50 const int kFileManagerWidth = 972; // pixels 51 const int kFileManagerHeight = 640; // pixels 52 const int kFileManagerMinimumWidth = 640; // pixels 53 const int kFileManagerMinimumHeight = 240; // pixels 54 55 // Holds references to file manager dialogs that have callbacks pending 56 // to their listeners. 57 class 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 74 PendingDialog* PendingDialog::GetInstance() { 75 return Singleton<PendingDialog>::get(); 76 } 77 78 void 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 87 void PendingDialog::Remove(SelectFileDialogExtension::RoutingID id) { 88 map_.erase(id); 89 } 90 91 scoped_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) 100 void 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|. 109 void 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 163 SelectFileDialogExtension::RoutingID 164 SelectFileDialogExtension::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 174 SelectFileDialogExtension* SelectFileDialogExtension::Create( 175 Listener* listener, 176 ui::SelectFilePolicy* policy) { 177 return new SelectFileDialogExtension(listener, policy); 178 } 179 180 SelectFileDialogExtension::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 193 SelectFileDialogExtension::~SelectFileDialogExtension() { 194 if (extension_dialog_.get()) 195 extension_dialog_->ObserverDestroyed(); 196 } 197 198 bool SelectFileDialogExtension::IsRunning( 199 gfx::NativeWindow owner_window) const { 200 return owner_window_ == owner_window; 201 } 202 203 void SelectFileDialogExtension::ListenerDestroyed() { 204 listener_ = NULL; 205 params_ = NULL; 206 PendingDialog::GetInstance()->Remove(routing_id_); 207 } 208 209 void 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 220 void 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 248 void 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 263 void 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 276 void 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 286 content::RenderViewHost* SelectFileDialogExtension::GetRenderViewHost() { 287 if (extension_dialog_.get()) 288 return extension_dialog_->host()->render_view_host(); 289 return NULL; 290 } 291 292 void 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 313 void SelectFileDialogExtension::AddPending(RoutingID routing_id) { 314 PendingDialog::GetInstance()->Add(routing_id, this); 315 } 316 317 // static 318 bool SelectFileDialogExtension::PendingExists(RoutingID routing_id) { 319 return PendingDialog::GetInstance()->Find(routing_id).get() != NULL; 320 } 321 322 bool SelectFileDialogExtension::HasMultipleFileTypeChoicesImpl() { 323 return has_multiple_file_type_choices_; 324 } 325 326 void 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 ¤t_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 ¤t_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