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