| OLD | NEW |
| (Empty) |
| 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/intents/web_intent_picker_controller.h" | |
| 6 | |
| 7 #include <vector> | |
| 8 | |
| 9 #include "base/bind.h" | |
| 10 #include "base/bind_helpers.h" | |
| 11 #include "base/md5.h" | |
| 12 #include "base/memory/scoped_ptr.h" | |
| 13 #include "base/time.h" | |
| 14 #include "base/utf_string_conversions.h" | |
| 15 #include "chrome/browser/download/download_item_model.h" | |
| 16 #include "chrome/browser/extensions/extension_service.h" | |
| 17 #include "chrome/browser/extensions/extension_system.h" | |
| 18 #include "chrome/browser/extensions/platform_app_launcher.h" | |
| 19 #include "chrome/browser/extensions/webstore_installer.h" | |
| 20 #include "chrome/browser/intents/cws_intents_registry_factory.h" | |
| 21 #include "chrome/browser/intents/default_web_intent_service.h" | |
| 22 #include "chrome/browser/intents/intent_service_host.h" | |
| 23 #include "chrome/browser/intents/native_services.h" | |
| 24 #include "chrome/browser/intents/web_intents_registry_factory.h" | |
| 25 #include "chrome/browser/intents/web_intents_reporting.h" | |
| 26 #include "chrome/browser/profiles/profile.h" | |
| 27 #include "chrome/browser/tab_contents/tab_util.h" | |
| 28 #include "chrome/browser/ui/browser.h" | |
| 29 #include "chrome/browser/ui/browser_finder.h" | |
| 30 #include "chrome/browser/ui/browser_list.h" | |
| 31 #include "chrome/browser/ui/browser_navigator.h" | |
| 32 #include "chrome/browser/ui/browser_tabstrip.h" | |
| 33 #include "chrome/browser/ui/browser_window.h" | |
| 34 #include "chrome/browser/ui/intents/web_intent_icon_loader.h" | |
| 35 #include "chrome/browser/ui/intents/web_intent_picker.h" | |
| 36 #include "chrome/browser/ui/intents/web_intent_picker_model.h" | |
| 37 #include "chrome/browser/ui/tabs/tab_strip_model.h" | |
| 38 #include "chrome/browser/ui/web_contents_modal_dialog_manager.h" | |
| 39 #include "chrome/browser/webdata/web_data_service.h" | |
| 40 #include "chrome/common/url_constants.h" | |
| 41 #include "content/public/browser/browser_thread.h" | |
| 42 #include "content/public/browser/download_manager.h" | |
| 43 #include "content/public/browser/web_contents.h" | |
| 44 #include "content/public/browser/web_contents_observer.h" | |
| 45 #include "content/public/browser/web_intents_dispatcher.h" | |
| 46 #include "ui/gfx/favicon_size.h" | |
| 47 #include "ui/gfx/image/image.h" | |
| 48 | |
| 49 using extensions::WebstoreInstaller; | |
| 50 | |
| 51 DEFINE_WEB_CONTENTS_USER_DATA_KEY(WebIntentPickerController); | |
| 52 | |
| 53 namespace { | |
| 54 | |
| 55 // Maximum amount of time to delay displaying dialog while waiting for data. | |
| 56 const int kMaxHiddenSetupTimeMs = 200; | |
| 57 | |
| 58 // Minimum amount of time to show waiting dialog, if it is shown. | |
| 59 const int kMinThrobberDisplayTimeMs = 800; | |
| 60 | |
| 61 // Gets the web intents registry for the specified profile. | |
| 62 WebIntentsRegistry* GetWebIntentsRegistry(Profile* profile) { | |
| 63 return WebIntentsRegistryFactory::GetForProfile(profile); | |
| 64 } | |
| 65 | |
| 66 // Gets the Chrome web store intents registry for the specified profile. | |
| 67 CWSIntentsRegistry* GetCWSIntentsRegistry(Profile* profile) { | |
| 68 return CWSIntentsRegistryFactory::GetForProfile(profile); | |
| 69 } | |
| 70 | |
| 71 class SourceWindowObserver : content::WebContentsObserver { | |
| 72 public: | |
| 73 SourceWindowObserver(content::WebContents* web_contents, | |
| 74 base::WeakPtr<WebIntentPickerController> controller) | |
| 75 : content::WebContentsObserver(web_contents), | |
| 76 controller_(controller) {} | |
| 77 virtual ~SourceWindowObserver() {} | |
| 78 | |
| 79 // Implement WebContentsObserver | |
| 80 virtual void WebContentsDestroyed( | |
| 81 content::WebContents* web_contents) OVERRIDE { | |
| 82 if (controller_) | |
| 83 controller_->SourceWebContentsDestroyed(web_contents); | |
| 84 delete this; | |
| 85 } | |
| 86 | |
| 87 private: | |
| 88 base::WeakPtr<WebIntentPickerController> controller_; | |
| 89 }; | |
| 90 | |
| 91 // Deletes |service|, presumably called from a dispatcher callback. | |
| 92 void DeleteIntentService( | |
| 93 web_intents::IntentServiceHost* service, | |
| 94 webkit_glue::WebIntentReplyType type) { | |
| 95 delete service; | |
| 96 } | |
| 97 | |
| 98 } // namespace | |
| 99 | |
| 100 // UMAReporter handles reporting Web Intents events to UMA. | |
| 101 class WebIntentPickerController::UMAReporter { | |
| 102 public: | |
| 103 | |
| 104 // Resets the service active duration timer to "now". | |
| 105 void ResetServiceActiveTimer(); | |
| 106 | |
| 107 // Records the duration of time spent using the service. Uses |reply_type| | |
| 108 // to distinguish between successful and failed service calls. | |
| 109 void RecordServiceActiveDuration(webkit_glue::WebIntentReplyType reply_type); | |
| 110 | |
| 111 private: | |
| 112 | |
| 113 // The time when the user began using the service. | |
| 114 base::TimeTicks service_start_time_; | |
| 115 }; | |
| 116 | |
| 117 void WebIntentPickerController::UMAReporter::ResetServiceActiveTimer() { | |
| 118 service_start_time_ = base::TimeTicks::Now(); | |
| 119 } | |
| 120 | |
| 121 void WebIntentPickerController::UMAReporter::RecordServiceActiveDuration( | |
| 122 webkit_glue::WebIntentReplyType reply_type) { | |
| 123 if (!service_start_time_.is_null()) { | |
| 124 web_intents::RecordServiceActiveDuration(reply_type, | |
| 125 base::TimeTicks::Now() - service_start_time_); | |
| 126 } | |
| 127 } | |
| 128 | |
| 129 WebIntentPickerController::WebIntentPickerController( | |
| 130 content::WebContents* web_contents) | |
| 131 : dialog_state_(kPickerHidden), | |
| 132 web_contents_(web_contents), | |
| 133 profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())), | |
| 134 picker_(NULL), | |
| 135 picker_model_(new WebIntentPickerModel()), | |
| 136 uma_reporter_(new UMAReporter()), | |
| 137 pending_async_count_(0), | |
| 138 pending_registry_calls_count_(0), | |
| 139 pending_cws_request_(false), | |
| 140 picker_shown_(false), | |
| 141 window_disposition_source_(NULL), | |
| 142 source_intents_dispatcher_(NULL), | |
| 143 intents_dispatcher_(NULL), | |
| 144 location_bar_button_indicated_(true), | |
| 145 service_tab_(NULL), | |
| 146 icon_loader_(NULL), | |
| 147 ALLOW_THIS_IN_INITIALIZER_LIST(weak_ptr_factory_(this)), | |
| 148 ALLOW_THIS_IN_INITIALIZER_LIST(timer_factory_(this)), | |
| 149 ALLOW_THIS_IN_INITIALIZER_LIST(dispatcher_factory_(this)) { | |
| 150 native_services_.reset(new web_intents::NativeServiceFactory()); | |
| 151 #if defined(TOOLKIT_VIEWS) | |
| 152 cancelled_ = true; | |
| 153 #endif | |
| 154 icon_loader_.reset( | |
| 155 new web_intents::IconLoader(profile_, picker_model_.get())); | |
| 156 } | |
| 157 | |
| 158 WebIntentPickerController::~WebIntentPickerController() { | |
| 159 if (picker_) | |
| 160 picker_->InvalidateDelegate(); | |
| 161 if (webstore_installer_.get()) | |
| 162 webstore_installer_->InvalidateDelegate(); | |
| 163 } | |
| 164 | |
| 165 // TODO(gbillock): combine this with ShowDialog. | |
| 166 void WebIntentPickerController::SetIntentsDispatcher( | |
| 167 content::WebIntentsDispatcher* intents_dispatcher) { | |
| 168 // TODO(gbillock): This is to account for multiple dispatches in the same tab. | |
| 169 // That is currently not a well-handled case, and this is a band-aid. | |
| 170 dispatcher_factory_.InvalidateWeakPtrs(); | |
| 171 intents_dispatcher_ = intents_dispatcher; | |
| 172 intents_dispatcher_->RegisterReplyNotification( | |
| 173 base::Bind(&WebIntentPickerController::OnSendReturnMessage, | |
| 174 dispatcher_factory_.GetWeakPtr())); | |
| 175 | |
| 176 // Initialize the reporting bucket. | |
| 177 const webkit_glue::WebIntentData& intent = intents_dispatcher_->GetIntent(); | |
| 178 uma_bucket_ = web_intents::ToUMABucket(intent.action, intent.type); | |
| 179 } | |
| 180 | |
| 181 // TODO(smckay): rename this "StartActivity". | |
| 182 void WebIntentPickerController::ShowDialog(const string16& action, | |
| 183 const string16& type) { | |
| 184 ShowDialog(kEnableDefaults); | |
| 185 } | |
| 186 | |
| 187 void WebIntentPickerController::ReshowDialog() { | |
| 188 ShowDialog(kSuppressDefaults); | |
| 189 } | |
| 190 | |
| 191 void WebIntentPickerController::ShowDialog(DefaultsUsage suppress_defaults) { | |
| 192 web_intents::RecordIntentDispatched(uma_bucket_); | |
| 193 | |
| 194 DCHECK(intents_dispatcher_); | |
| 195 | |
| 196 #if defined(TOOLKIT_VIEWS) | |
| 197 cancelled_ = true; | |
| 198 #endif | |
| 199 | |
| 200 // Only show a picker once. | |
| 201 // TODO(gbillock): There's a hole potentially admitting multiple | |
| 202 // in-flight dispatches since we don't create the picker | |
| 203 // in this method, but only after calling the registry. | |
| 204 if (picker_shown_) { | |
| 205 intents_dispatcher_->SendReply(webkit_glue::WebIntentReply( | |
| 206 webkit_glue::WEB_INTENT_REPLY_FAILURE, | |
| 207 ASCIIToUTF16("Simultaneous intent invocation."))); | |
| 208 return; | |
| 209 } | |
| 210 | |
| 211 // TODO(binji): Figure out what to do when intents are invoked from incognito | |
| 212 // mode. | |
| 213 if (profile_->IsOffTheRecord()) { | |
| 214 intents_dispatcher_->SendReply(webkit_glue::WebIntentReply( | |
| 215 webkit_glue::WEB_INTENT_REPLY_FAILURE, string16())); | |
| 216 return; | |
| 217 } | |
| 218 | |
| 219 picker_model_->Clear(); | |
| 220 picker_model_->set_action(intents_dispatcher_->GetIntent().action); | |
| 221 picker_model_->set_type(intents_dispatcher_->GetIntent().type); | |
| 222 | |
| 223 // If the intent is explicit, skip showing the picker. | |
| 224 const GURL& service = intents_dispatcher_->GetIntent().service; | |
| 225 // TODO(gbillock): Decide whether to honor the default suppression flag | |
| 226 // here or suppress the control for explicit intents. | |
| 227 if (service.is_valid() && !suppress_defaults) { | |
| 228 // Get services from the registry to verify a registered extension | |
| 229 // page for this action/type if it is permitted to be dispatched. (Also | |
| 230 // required to find disposition set by service.) | |
| 231 pending_async_count_++; | |
| 232 GetWebIntentsRegistry(profile_)->GetIntentServices( | |
| 233 picker_model_->action(), picker_model_->type(), | |
| 234 base::Bind( | |
| 235 &WebIntentPickerController:: | |
| 236 OnWebIntentServicesAvailableForExplicitIntent, | |
| 237 weak_ptr_factory_.GetWeakPtr())); | |
| 238 return; | |
| 239 } | |
| 240 | |
| 241 // As soon as the dialog is requested, block all input events | |
| 242 // on the original tab. | |
| 243 WebContentsModalDialogManager* web_contents_modal_dialog_manager = | |
| 244 WebContentsModalDialogManager::FromWebContents(web_contents_); | |
| 245 web_contents_modal_dialog_manager->BlockWebContentsInteraction(true); | |
| 246 SetDialogState(kPickerSetup); | |
| 247 | |
| 248 pending_async_count_++; | |
| 249 pending_registry_calls_count_++; | |
| 250 GetWebIntentsRegistry(profile_)->GetIntentServices( | |
| 251 picker_model_->action(), picker_model_->type(), | |
| 252 base::Bind(&WebIntentPickerController::OnWebIntentServicesAvailable, | |
| 253 weak_ptr_factory_.GetWeakPtr())); | |
| 254 | |
| 255 GURL invoking_url = web_contents_->GetURL(); | |
| 256 if (invoking_url.is_valid() && !suppress_defaults) { | |
| 257 pending_async_count_++; | |
| 258 pending_registry_calls_count_++; | |
| 259 GetWebIntentsRegistry(profile_)->GetDefaultIntentService( | |
| 260 picker_model_->action(), picker_model_->type(), invoking_url, | |
| 261 base::Bind(&WebIntentPickerController::OnWebIntentDefaultsAvailable, | |
| 262 weak_ptr_factory_.GetWeakPtr())); | |
| 263 } | |
| 264 | |
| 265 pending_cws_request_ = true; | |
| 266 pending_async_count_++; | |
| 267 GetCWSIntentsRegistry(profile_)->GetIntentServices( | |
| 268 picker_model_->action(), picker_model_->type(), | |
| 269 base::Bind(&WebIntentPickerController::OnCWSIntentServicesAvailable, | |
| 270 weak_ptr_factory_.GetWeakPtr())); | |
| 271 } | |
| 272 | |
| 273 void WebIntentPickerController::OnServiceChosen( | |
| 274 const GURL& url, | |
| 275 webkit_glue::WebIntentServiceData::Disposition disposition, | |
| 276 DefaultsUsage suppress_defaults) { | |
| 277 web_intents::RecordServiceInvoke(uma_bucket_); | |
| 278 uma_reporter_->ResetServiceActiveTimer(); | |
| 279 ExtensionService* extension_service = | |
| 280 extensions::ExtensionSystem::Get(profile_)->extension_service(); | |
| 281 DCHECK(extension_service); | |
| 282 | |
| 283 #if defined(TOOLKIT_VIEWS) | |
| 284 cancelled_ = false; | |
| 285 #endif | |
| 286 | |
| 287 // Set the default here. Activating the intent resets the picker model. | |
| 288 // TODO(gbillock): we should perhaps couple the model to the dispatcher so | |
| 289 // we can re-activate the model on use-another-service. | |
| 290 if (!suppress_defaults) | |
| 291 SetDefaultServiceForSelection(url); | |
| 292 | |
| 293 | |
| 294 // TODO(smckay): this basically smells like another disposition. | |
| 295 const extensions::Extension* extension = | |
| 296 extension_service->GetInstalledApp(url); | |
| 297 if (extension && extension->is_platform_app()) { | |
| 298 extensions::LaunchPlatformAppWithWebIntent(profile_, | |
| 299 extension, intents_dispatcher_, web_contents_); | |
| 300 ClosePicker(); | |
| 301 return; | |
| 302 } | |
| 303 | |
| 304 // TODO(smckay): This entire method shold basically be pulled out | |
| 305 // into a separate class dedicated to the execution of intents. | |
| 306 // The tricky part is with the "INLINE" disposition where we | |
| 307 // want to (re)use the picker to handle the intent. A bit of | |
| 308 // artful composition + lazy instantiation should make that possible. | |
| 309 switch (disposition) { | |
| 310 case webkit_glue::WebIntentServiceData::DISPOSITION_NATIVE: { | |
| 311 web_intents::IntentServiceHost* service = | |
| 312 native_services_->CreateServiceInstance( | |
| 313 url, intents_dispatcher_->GetIntent(), web_contents_); | |
| 314 DCHECK(service); | |
| 315 | |
| 316 intents_dispatcher_->RegisterReplyNotification( | |
| 317 base::Bind(&DeleteIntentService, base::Unretained(service))); | |
| 318 | |
| 319 service->HandleIntent(intents_dispatcher_); | |
| 320 break; | |
| 321 } | |
| 322 | |
| 323 case webkit_glue::WebIntentServiceData::DISPOSITION_INLINE: { | |
| 324 // Set the model to inline disposition. It will notify the picker which | |
| 325 // will respond (via OnInlineDispositionWebContentsCreated) with the | |
| 326 // WebContents to dispatch the intent to. | |
| 327 picker_model_->SetInlineDisposition(url); | |
| 328 break; | |
| 329 } | |
| 330 | |
| 331 case webkit_glue::WebIntentServiceData::DISPOSITION_WINDOW: { | |
| 332 content::WebContents* contents = content::WebContents::Create( | |
| 333 content::WebContents::CreateParams( | |
| 334 profile_, tab_util::GetSiteInstanceForNewTab(profile_, url))); | |
| 335 WebIntentPickerController::CreateForWebContents(contents); | |
| 336 | |
| 337 // Let the controller for the target WebContents know that it is hosting a | |
| 338 // web intents service. Suppress if we're not showing the | |
| 339 // use-another-service button. | |
| 340 if (picker_model_->show_use_another_service()) { | |
| 341 WebIntentPickerController::FromWebContents(contents)-> | |
| 342 SetWindowDispositionSource(web_contents_, intents_dispatcher_); | |
| 343 } | |
| 344 | |
| 345 intents_dispatcher_->DispatchIntent(contents); | |
| 346 service_tab_ = contents; | |
| 347 | |
| 348 // This call performs all the tab strip manipulation, notifications, etc. | |
| 349 // Since we're passing in a target_contents, it assumes that we will | |
| 350 // navigate the page ourselves, though. | |
| 351 chrome::NavigateParams params(profile_, url, | |
| 352 content::PAGE_TRANSITION_LINK); | |
| 353 params.target_contents = contents; | |
| 354 params.disposition = NEW_FOREGROUND_TAB; | |
| 355 params.tabstrip_add_types = TabStripModel::ADD_INHERIT_GROUP; | |
| 356 chrome::Navigate(¶ms); | |
| 357 | |
| 358 service_tab_->GetController().LoadURL( | |
| 359 url, content::Referrer(), | |
| 360 content::PAGE_TRANSITION_AUTO_BOOKMARK, std::string()); | |
| 361 | |
| 362 ClosePicker(); | |
| 363 break; | |
| 364 } | |
| 365 | |
| 366 default: | |
| 367 NOTREACHED(); | |
| 368 break; | |
| 369 } | |
| 370 } | |
| 371 | |
| 372 content::WebContents* | |
| 373 WebIntentPickerController::CreateWebContentsForInlineDisposition( | |
| 374 Profile* profile, const GURL& url) { | |
| 375 content::WebContents::CreateParams create_params( | |
| 376 profile, tab_util::GetSiteInstanceForNewTab(profile, url)); | |
| 377 content::WebContents* web_contents = content::WebContents::Create( | |
| 378 create_params); | |
| 379 intents_dispatcher_->DispatchIntent(web_contents); | |
| 380 return web_contents; | |
| 381 } | |
| 382 | |
| 383 void WebIntentPickerController::SetDefaultServiceForSelection(const GURL& url) { | |
| 384 DCHECK(picker_model_.get()); | |
| 385 if (url == picker_model_->default_service_url()) | |
| 386 return; | |
| 387 | |
| 388 DefaultWebIntentService record; | |
| 389 record.action = picker_model_->action(); | |
| 390 record.type = picker_model_->type(); | |
| 391 record.service_url = url.spec(); | |
| 392 record.user_date = static_cast<int>(floor(base::Time::Now().ToDoubleT())); | |
| 393 GetWebIntentsRegistry(profile_)->RegisterDefaultIntentService(record); | |
| 394 } | |
| 395 | |
| 396 void WebIntentPickerController::OnExtensionInstallRequested( | |
| 397 const std::string& id) { | |
| 398 // Create a local copy of |id| since it is a reference to a member on a UI | |
| 399 // object, and SetPendingExtensionInstallId triggers an OnModelChanged and | |
| 400 // a subsequent rebuild of UI objects. | |
| 401 std::string extension_id(id); | |
| 402 | |
| 403 picker_model_->SetPendingExtensionInstallId(extension_id); | |
| 404 | |
| 405 scoped_ptr<WebstoreInstaller::Approval> approval( | |
| 406 WebstoreInstaller::Approval::CreateWithInstallPrompt(profile_)); | |
| 407 // Don't show a bubble pointing to the extension or any other post | |
| 408 // installation UI. | |
| 409 approval->skip_post_install_ui = true; | |
| 410 approval->show_dialog_callback = base::Bind( | |
| 411 &WebIntentPickerController::OnShowExtensionInstallDialog, | |
| 412 weak_ptr_factory_.GetWeakPtr()); | |
| 413 | |
| 414 webstore_installer_ = new WebstoreInstaller(profile_, this, | |
| 415 &web_contents_->GetController(), extension_id, | |
| 416 approval.Pass(), WebstoreInstaller::FLAG_INLINE_INSTALL); | |
| 417 | |
| 418 pending_async_count_++; | |
| 419 webstore_installer_->Start(); | |
| 420 } | |
| 421 | |
| 422 void WebIntentPickerController::OnExtensionLinkClicked( | |
| 423 const std::string& id, | |
| 424 WindowOpenDisposition disposition) { | |
| 425 // Navigate from source tab. | |
| 426 GURL extension_url(extension_urls::GetWebstoreItemDetailURLPrefix() + id); | |
| 427 chrome::NavigateParams params(profile_, extension_url, | |
| 428 content::PAGE_TRANSITION_LINK); | |
| 429 params.disposition = | |
| 430 (disposition == CURRENT_TAB) ? NEW_FOREGROUND_TAB : disposition; | |
| 431 chrome::Navigate(¶ms); | |
| 432 } | |
| 433 | |
| 434 void WebIntentPickerController::OnSuggestionsLinkClicked( | |
| 435 WindowOpenDisposition disposition) { | |
| 436 // Navigate from source tab. | |
| 437 GURL query_url = extension_urls::GetWebstoreIntentQueryURL( | |
| 438 UTF16ToUTF8(picker_model_->action()), | |
| 439 UTF16ToUTF8(picker_model_->type())); | |
| 440 chrome::NavigateParams params(profile_, query_url, | |
| 441 content::PAGE_TRANSITION_LINK); | |
| 442 params.disposition = | |
| 443 (disposition == CURRENT_TAB) ? NEW_FOREGROUND_TAB : disposition; | |
| 444 chrome::Navigate(¶ms); | |
| 445 } | |
| 446 | |
| 447 void WebIntentPickerController::OnUserCancelledPickerDialog() { | |
| 448 if (!intents_dispatcher_) | |
| 449 return; | |
| 450 | |
| 451 intents_dispatcher_->SendReply(webkit_glue::WebIntentReply( | |
| 452 webkit_glue::WEB_INTENT_PICKER_CANCELLED, string16())); | |
| 453 web_intents::RecordPickerCancel(uma_bucket_); | |
| 454 | |
| 455 ClosePicker(); | |
| 456 } | |
| 457 | |
| 458 void WebIntentPickerController::OnChooseAnotherService() { | |
| 459 DCHECK(intents_dispatcher_); | |
| 460 web_intents::RecordChooseAnotherService(uma_bucket_); | |
| 461 intents_dispatcher_->ResetDispatch(); | |
| 462 picker_model_->SetInlineDisposition(GURL::EmptyGURL()); | |
| 463 } | |
| 464 | |
| 465 void WebIntentPickerController::OnClosing() { | |
| 466 SetDialogState(kPickerHidden); | |
| 467 picker_ = NULL; | |
| 468 picker_model_->ClearPendingExtensionInstall(); | |
| 469 CancelDownload(); | |
| 470 #if defined(TOOLKIT_VIEWS) | |
| 471 if (cancelled_) | |
| 472 OnUserCancelledPickerDialog(); | |
| 473 #endif | |
| 474 } | |
| 475 | |
| 476 void WebIntentPickerController::OnExtensionDownloadStarted( | |
| 477 const std::string& id, | |
| 478 content::DownloadItem* item) { | |
| 479 DownloadItemModel(item).SetShouldShowInShelf(false); | |
| 480 download_id_ = item->GetGlobalId(); | |
| 481 picker_model_->UpdateExtensionDownloadState(item); | |
| 482 } | |
| 483 | |
| 484 void WebIntentPickerController::OnExtensionDownloadProgress( | |
| 485 const std::string& id, | |
| 486 content::DownloadItem* item) { | |
| 487 picker_model_->UpdateExtensionDownloadState(item); | |
| 488 } | |
| 489 | |
| 490 void WebIntentPickerController::OnExtensionInstallSuccess( | |
| 491 const std::string& extension_id) { | |
| 492 webstore_installer_ = NULL; // Release reference. | |
| 493 | |
| 494 // OnExtensionInstallSuccess is called via NotificationService::Notify before | |
| 495 // the extension is added to the ExtensionService. Dispatch via PostTask to | |
| 496 // allow ExtensionService to update. | |
| 497 MessageLoop::current()->PostTask( | |
| 498 FROM_HERE, | |
| 499 base::Bind( | |
| 500 &WebIntentPickerController::DispatchToInstalledExtension, | |
| 501 base::Unretained(this), | |
| 502 extension_id)); | |
| 503 } | |
| 504 | |
| 505 void WebIntentPickerController::DispatchToInstalledExtension( | |
| 506 const std::string& extension_id) { | |
| 507 web_intents::RecordCWSExtensionInstalled(uma_bucket_); | |
| 508 | |
| 509 download_id_ = content::DownloadId(); | |
| 510 picker_model_->ClearPendingExtensionInstall(); | |
| 511 if (picker_) | |
| 512 picker_->OnExtensionInstallSuccess(extension_id); | |
| 513 | |
| 514 WebIntentsRegistry::IntentServiceList services; | |
| 515 GetWebIntentsRegistry(profile_)->GetIntentServicesForExtensionFilter( | |
| 516 picker_model_->action(), picker_model_->type(), | |
| 517 extension_id, | |
| 518 &services); | |
| 519 | |
| 520 // Extension must be registered with registry by now. | |
| 521 DCHECK(services.size() > 0); | |
| 522 | |
| 523 // TODO(binji): We're going to need to disambiguate if there are multiple | |
| 524 // services. For now, just choose the first. | |
| 525 const webkit_glue::WebIntentServiceData& service_data = services[0]; | |
| 526 | |
| 527 picker_model_->RemoveSuggestedExtension(extension_id); | |
| 528 AddServiceToModel(service_data); | |
| 529 OnServiceChosen(service_data.service_url, service_data.disposition, | |
| 530 kEnableDefaults); | |
| 531 AsyncOperationFinished(); | |
| 532 } | |
| 533 | |
| 534 void WebIntentPickerController::OnExtensionInstallFailure( | |
| 535 const std::string& id, | |
| 536 const std::string& error, | |
| 537 WebstoreInstaller::FailureReason reason) { | |
| 538 webstore_installer_ = NULL; // Release reference. | |
| 539 | |
| 540 // If the user cancelled the install then don't show an error message. | |
| 541 if (reason == WebstoreInstaller::FAILURE_REASON_CANCELLED) | |
| 542 picker_model_->ClearPendingExtensionInstall(); | |
| 543 else | |
| 544 picker_model_->SetPendingExtensionInstallStatusString(UTF8ToUTF16(error)); | |
| 545 | |
| 546 if (picker_) | |
| 547 picker_->OnExtensionInstallFailure(id); | |
| 548 AsyncOperationFinished(); | |
| 549 } | |
| 550 | |
| 551 void WebIntentPickerController::OnSendReturnMessage( | |
| 552 webkit_glue::WebIntentReplyType reply_type) { | |
| 553 ClosePicker(); | |
| 554 uma_reporter_->RecordServiceActiveDuration(reply_type); | |
| 555 | |
| 556 if (service_tab_ && | |
| 557 reply_type != webkit_glue::WEB_INTENT_SERVICE_CONTENTS_CLOSED) { | |
| 558 Browser* browser = chrome::FindBrowserWithWebContents(service_tab_); | |
| 559 if (browser) { | |
| 560 int index = browser->tab_strip_model()->GetIndexOfWebContents( | |
| 561 service_tab_); | |
| 562 browser->tab_strip_model()->CloseWebContentsAt( | |
| 563 index, TabStripModel::CLOSE_CREATE_HISTORICAL_TAB); | |
| 564 | |
| 565 // Activate source tab. | |
| 566 Browser* source_browser = | |
| 567 chrome::FindBrowserWithWebContents(web_contents_); | |
| 568 if (source_browser) { | |
| 569 int source_index = source_browser->tab_strip_model()-> | |
| 570 GetIndexOfWebContents(web_contents_); | |
| 571 source_browser->tab_strip_model()->ActivateTabAt(source_index, false); | |
| 572 } | |
| 573 } | |
| 574 service_tab_ = NULL; | |
| 575 } | |
| 576 | |
| 577 intents_dispatcher_ = NULL; | |
| 578 } | |
| 579 | |
| 580 void WebIntentPickerController::AddServiceToModel( | |
| 581 const webkit_glue::WebIntentServiceData& service) { | |
| 582 | |
| 583 picker_model_->AddInstalledService( | |
| 584 service.title, | |
| 585 service.service_url, | |
| 586 service.disposition); | |
| 587 | |
| 588 icon_loader_->LoadFavicon(service.service_url); | |
| 589 } | |
| 590 | |
| 591 void WebIntentPickerController::OnWebIntentServicesAvailable( | |
| 592 const std::vector<webkit_glue::WebIntentServiceData>& services) { | |
| 593 for (size_t i = 0; i < services.size(); ++i) | |
| 594 AddServiceToModel(services[i]); | |
| 595 | |
| 596 RegistryCallsCompleted(); | |
| 597 AsyncOperationFinished(); | |
| 598 } | |
| 599 | |
| 600 void WebIntentPickerController::OnWebIntentServicesAvailableForExplicitIntent( | |
| 601 const std::vector<webkit_glue::WebIntentServiceData>& services) { | |
| 602 DCHECK(intents_dispatcher_); | |
| 603 DCHECK(intents_dispatcher_->GetIntent().service.is_valid()); | |
| 604 for (size_t i = 0; i < services.size(); ++i) { | |
| 605 if (services[i].service_url != intents_dispatcher_->GetIntent().service) | |
| 606 continue; | |
| 607 | |
| 608 InvokeServiceWithSelection(services[i]); | |
| 609 AsyncOperationFinished(); | |
| 610 return; | |
| 611 } | |
| 612 | |
| 613 // No acceptable extension. The intent cannot be dispatched. | |
| 614 intents_dispatcher_->SendReply(webkit_glue::WebIntentReply( | |
| 615 webkit_glue::WEB_INTENT_REPLY_FAILURE, ASCIIToUTF16( | |
| 616 "Explicit extension URL is not available."))); | |
| 617 | |
| 618 AsyncOperationFinished(); | |
| 619 } | |
| 620 | |
| 621 void WebIntentPickerController::OnWebIntentDefaultsAvailable( | |
| 622 const DefaultWebIntentService& default_service) { | |
| 623 if (!default_service.service_url.empty()) { | |
| 624 // TODO(gbillock): this doesn't belong in the model, but keep it there | |
| 625 // for now. | |
| 626 picker_model_->set_default_service_url(GURL(default_service.service_url)); | |
| 627 } | |
| 628 | |
| 629 RegistryCallsCompleted(); | |
| 630 AsyncOperationFinished(); | |
| 631 } | |
| 632 | |
| 633 void WebIntentPickerController::RegistryCallsCompleted() { | |
| 634 pending_registry_calls_count_--; | |
| 635 if (pending_registry_calls_count_ != 0) return; | |
| 636 | |
| 637 if (picker_model_->default_service_url().is_valid()) { | |
| 638 // If there's a default service, dispatch to it immediately | |
| 639 // without showing the picker. | |
| 640 const WebIntentPickerModel::InstalledService* default_service = | |
| 641 picker_model_->GetInstalledServiceWithURL( | |
| 642 GURL(picker_model_->default_service_url())); | |
| 643 if (default_service != NULL) { | |
| 644 InvokeService(*default_service); | |
| 645 return; | |
| 646 } | |
| 647 } | |
| 648 | |
| 649 OnPickerEvent(kPickerEventRegistryDataComplete); | |
| 650 OnIntentDataArrived(); | |
| 651 } | |
| 652 | |
| 653 void WebIntentPickerController::OnCWSIntentServicesAvailable( | |
| 654 const CWSIntentsRegistry::IntentExtensionList& extensions) { | |
| 655 ExtensionServiceInterface* extension_service = | |
| 656 extensions::ExtensionSystem::Get(profile_)->extension_service(); | |
| 657 | |
| 658 std::vector<WebIntentPickerModel::SuggestedExtension> suggestions; | |
| 659 for (size_t i = 0; i < extensions.size(); ++i) { | |
| 660 const CWSIntentsRegistry::IntentExtensionInfo& info = extensions[i]; | |
| 661 | |
| 662 // Do not include suggestions for already installed extensions. | |
| 663 if (extension_service->GetExtensionById(info.id, true)) | |
| 664 continue; | |
| 665 | |
| 666 suggestions.push_back(WebIntentPickerModel::SuggestedExtension( | |
| 667 info.name, info.id, info.average_rating)); | |
| 668 | |
| 669 icon_loader_->LoadExtensionIcon(info.icon_url, info.id); | |
| 670 } | |
| 671 | |
| 672 picker_model_->AddSuggestedExtensions(suggestions); | |
| 673 | |
| 674 AsyncOperationFinished(); | |
| 675 pending_cws_request_ = false; | |
| 676 OnIntentDataArrived(); | |
| 677 } | |
| 678 | |
| 679 | |
| 680 void WebIntentPickerController::OnIntentDataArrived() { | |
| 681 DCHECK(picker_model_.get()); | |
| 682 | |
| 683 if (!pending_cws_request_ && | |
| 684 pending_registry_calls_count_ == 0) | |
| 685 OnPickerEvent(kPickerEventAsyncDataComplete); | |
| 686 } | |
| 687 | |
| 688 void WebIntentPickerController::Reset() { | |
| 689 // Abandon all callbacks. | |
| 690 weak_ptr_factory_.InvalidateWeakPtrs(); | |
| 691 timer_factory_.InvalidateWeakPtrs(); | |
| 692 | |
| 693 // Reset state associated with callbacks. | |
| 694 pending_async_count_ = 0; | |
| 695 pending_registry_calls_count_ = 0; | |
| 696 pending_cws_request_ = false; | |
| 697 | |
| 698 // Reset picker. | |
| 699 icon_loader_.reset(); | |
| 700 picker_model_.reset(new WebIntentPickerModel()); | |
| 701 icon_loader_.reset( | |
| 702 new web_intents::IconLoader(profile_, picker_model_.get())); | |
| 703 | |
| 704 picker_shown_ = false; | |
| 705 | |
| 706 DCHECK(web_contents_); | |
| 707 WebContentsModalDialogManager* web_contents_modal_dialog_manager = | |
| 708 WebContentsModalDialogManager::FromWebContents(web_contents_); | |
| 709 web_contents_modal_dialog_manager->BlockWebContentsInteraction(false); | |
| 710 } | |
| 711 | |
| 712 void WebIntentPickerController::OnShowExtensionInstallDialog( | |
| 713 const ExtensionInstallPrompt::ShowParams& show_params, | |
| 714 ExtensionInstallPrompt::Delegate* delegate, | |
| 715 const ExtensionInstallPrompt::Prompt& prompt) { | |
| 716 picker_model_->SetPendingExtensionInstallDelegate(delegate); | |
| 717 picker_model_->SetPendingExtensionInstallPrompt(prompt); | |
| 718 if (picker_) | |
| 719 picker_->OnShowExtensionInstallDialog(show_params, delegate, prompt); | |
| 720 } | |
| 721 | |
| 722 void WebIntentPickerController::SetWindowDispositionSource( | |
| 723 content::WebContents* source, | |
| 724 content::WebIntentsDispatcher* dispatcher) { | |
| 725 DCHECK(source); | |
| 726 DCHECK(dispatcher); | |
| 727 location_bar_button_indicated_ = false; | |
| 728 window_disposition_source_ = source; | |
| 729 if (window_disposition_source_) { | |
| 730 // This object is self-deleting when the source WebContents is destroyed. | |
| 731 new SourceWindowObserver(window_disposition_source_, | |
| 732 weak_ptr_factory_.GetWeakPtr()); | |
| 733 } | |
| 734 | |
| 735 if (dispatcher) { | |
| 736 dispatcher->RegisterReplyNotification( | |
| 737 base::Bind(&WebIntentPickerController::SourceDispatcherReplied, | |
| 738 weak_ptr_factory_.GetWeakPtr())); | |
| 739 } | |
| 740 source_intents_dispatcher_ = dispatcher; | |
| 741 } | |
| 742 | |
| 743 void WebIntentPickerController::SourceWebContentsDestroyed( | |
| 744 content::WebContents* source) { | |
| 745 window_disposition_source_ = NULL; | |
| 746 // TODO(gbillock): redraw location bar to kill button | |
| 747 } | |
| 748 | |
| 749 void WebIntentPickerController::SourceDispatcherReplied( | |
| 750 webkit_glue::WebIntentReplyType reply_type) { | |
| 751 source_intents_dispatcher_ = NULL; | |
| 752 // TODO(gbillock): redraw location bar to kill button | |
| 753 } | |
| 754 | |
| 755 bool WebIntentPickerController::ShowLocationBarPickerButton() { | |
| 756 return window_disposition_source_ || source_intents_dispatcher_; | |
| 757 } | |
| 758 | |
| 759 void WebIntentPickerController::OnPickerEvent(WebIntentPickerEvent event) { | |
| 760 switch (event) { | |
| 761 case kPickerEventHiddenSetupTimeout: | |
| 762 DCHECK(dialog_state_ == kPickerSetup); | |
| 763 SetDialogState(kPickerWaiting); | |
| 764 break; | |
| 765 | |
| 766 case kPickerEventMaxWaitTimeExceeded: | |
| 767 DCHECK(dialog_state_ == kPickerWaiting); | |
| 768 | |
| 769 // If registry data is complete, go to main dialog. Otherwise, wait. | |
| 770 if (pending_registry_calls_count_ == 0) | |
| 771 SetDialogState(kPickerMain); | |
| 772 else | |
| 773 SetDialogState(kPickerWaitLong); | |
| 774 break; | |
| 775 | |
| 776 case kPickerEventRegistryDataComplete: | |
| 777 DCHECK(dialog_state_ == kPickerSetup || | |
| 778 dialog_state_ == kPickerWaiting || | |
| 779 dialog_state_ == kPickerWaitLong); | |
| 780 | |
| 781 // If minimum wait dialog time is exceeded, display main dialog. | |
| 782 // Either way, we don't do a thing. | |
| 783 break; | |
| 784 | |
| 785 case kPickerEventAsyncDataComplete: | |
| 786 DCHECK(dialog_state_ == kPickerSetup || | |
| 787 dialog_state_ == kPickerWaiting || | |
| 788 dialog_state_ == kPickerWaitLong || | |
| 789 dialog_state_ == kPickerInline); | |
| 790 | |
| 791 // In setup state, transition to main dialog. In waiting state, let | |
| 792 // timer expire. | |
| 793 if (dialog_state_ == kPickerSetup) | |
| 794 SetDialogState(kPickerMain); | |
| 795 break; | |
| 796 | |
| 797 default: | |
| 798 NOTREACHED(); | |
| 799 break; | |
| 800 } | |
| 801 } | |
| 802 | |
| 803 void WebIntentPickerController::LocationBarPickerButtonClicked() { | |
| 804 DCHECK(web_contents_); | |
| 805 if (window_disposition_source_ && source_intents_dispatcher_) { | |
| 806 Browser* service_browser = | |
| 807 chrome::FindBrowserWithWebContents(web_contents_); | |
| 808 if (!service_browser) return; | |
| 809 | |
| 810 Browser* client_browser = | |
| 811 chrome::FindBrowserWithWebContents(window_disposition_source_); | |
| 812 if (!client_browser) | |
| 813 return; | |
| 814 int client_index = client_browser->tab_strip_model()->GetIndexOfWebContents( | |
| 815 window_disposition_source_); | |
| 816 DCHECK(client_index != TabStripModel::kNoTab); | |
| 817 | |
| 818 source_intents_dispatcher_->ResetDispatch(); | |
| 819 | |
| 820 WebIntentPickerController* client_controller = | |
| 821 WebIntentPickerController::FromWebContents(window_disposition_source_); | |
| 822 DCHECK(client_controller); | |
| 823 | |
| 824 // This call deletes this object, so anything below here needs to | |
| 825 // use stack variables. | |
| 826 chrome::CloseWebContents(service_browser, web_contents_, true); | |
| 827 | |
| 828 // Re-open the other tab and activate the picker. | |
| 829 client_browser->window()->Activate(); | |
| 830 client_browser->tab_strip_model()->ActivateTabAt(client_index, true); | |
| 831 // The picker has been Reset() when the new tab is created; need to fully | |
| 832 // reload. | |
| 833 client_controller->ReshowDialog(); | |
| 834 } | |
| 835 // TODO(gbillock): figure out what we ought to do in this case. Probably | |
| 836 // nothing? Refresh the location bar? | |
| 837 } | |
| 838 | |
| 839 void WebIntentPickerController::AsyncOperationFinished() { | |
| 840 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); | |
| 841 if (--pending_async_count_ == 0) { | |
| 842 if (picker_) | |
| 843 picker_->OnPendingAsyncCompleted(); | |
| 844 } | |
| 845 } | |
| 846 | |
| 847 void WebIntentPickerController::InvokeServiceWithSelection( | |
| 848 const webkit_glue::WebIntentServiceData& service) { | |
| 849 if (picker_shown_) { | |
| 850 intents_dispatcher_->SendReply(webkit_glue::WebIntentReply( | |
| 851 webkit_glue::WEB_INTENT_REPLY_FAILURE, | |
| 852 ASCIIToUTF16("Simultaneous intent invocation."))); | |
| 853 return; | |
| 854 } | |
| 855 | |
| 856 // TODO(gbillock): this is a bit hacky and exists because in the inline case, | |
| 857 // the picker currently assumes it exists. | |
| 858 AddServiceToModel(service); | |
| 859 picker_model_->set_show_use_another_service(false); | |
| 860 | |
| 861 if (service.disposition == | |
| 862 webkit_glue::WebIntentServiceData::DISPOSITION_INLINE) { | |
| 863 picker_model_->SetInlineDisposition(service.service_url); | |
| 864 SetDialogState(kPickerInline); | |
| 865 return; | |
| 866 } | |
| 867 | |
| 868 OnServiceChosen(service.service_url, service.disposition, kSuppressDefaults); | |
| 869 } | |
| 870 | |
| 871 void WebIntentPickerController::InvokeService( | |
| 872 const WebIntentPickerModel::InstalledService& service) { | |
| 873 if (service.disposition == | |
| 874 webkit_glue::WebIntentServiceData::DISPOSITION_INLINE) { | |
| 875 // This call will ensure the picker dialog is created and initialized. | |
| 876 picker_model_->SetInlineDisposition(service.url); | |
| 877 SetDialogState(kPickerInline); | |
| 878 return; | |
| 879 } | |
| 880 OnServiceChosen(service.url, service.disposition, kEnableDefaults); | |
| 881 } | |
| 882 | |
| 883 void WebIntentPickerController::SetDialogState(WebIntentPickerState state) { | |
| 884 // Ignore events that don't change state. | |
| 885 if (state == dialog_state_) | |
| 886 return; | |
| 887 | |
| 888 // Any pending timers are abandoned on state changes. | |
| 889 timer_factory_.InvalidateWeakPtrs(); | |
| 890 | |
| 891 switch (state) { | |
| 892 case kPickerSetup: | |
| 893 DCHECK_EQ(dialog_state_, kPickerHidden); | |
| 894 // Post timer CWS pending | |
| 895 MessageLoop::current()->PostDelayedTask(FROM_HERE, | |
| 896 base::Bind(&WebIntentPickerController::OnPickerEvent, | |
| 897 timer_factory_.GetWeakPtr(), | |
| 898 kPickerEventHiddenSetupTimeout), | |
| 899 base::TimeDelta::FromMilliseconds(kMaxHiddenSetupTimeMs)); | |
| 900 break; | |
| 901 | |
| 902 case kPickerWaiting: | |
| 903 DCHECK_EQ(dialog_state_, kPickerSetup); | |
| 904 // Waiting dialog can be dismissed after minimum wait time. | |
| 905 MessageLoop::current()->PostDelayedTask(FROM_HERE, | |
| 906 base::Bind(&WebIntentPickerController::OnPickerEvent, | |
| 907 timer_factory_.GetWeakPtr(), | |
| 908 kPickerEventMaxWaitTimeExceeded), | |
| 909 base::TimeDelta::FromMilliseconds(kMinThrobberDisplayTimeMs)); | |
| 910 break; | |
| 911 | |
| 912 case kPickerWaitLong: | |
| 913 DCHECK_EQ(dialog_state_, kPickerWaiting); | |
| 914 break; | |
| 915 | |
| 916 case kPickerInline: | |
| 917 // Intentional fall-through. | |
| 918 case kPickerMain: | |
| 919 // No DCHECK - main state can be reached from any state. | |
| 920 // Ready to display data. | |
| 921 picker_model_->SetWaitingForSuggestions(false); | |
| 922 break; | |
| 923 | |
| 924 case kPickerHidden: | |
| 925 Reset(); | |
| 926 break; | |
| 927 | |
| 928 default: | |
| 929 NOTREACHED(); | |
| 930 break; | |
| 931 | |
| 932 } | |
| 933 | |
| 934 dialog_state_ = state; | |
| 935 | |
| 936 // Create picker dialog when changing away from hidden state. | |
| 937 if (dialog_state_ != kPickerHidden && dialog_state_ != kPickerSetup) | |
| 938 CreatePicker(); | |
| 939 } | |
| 940 | |
| 941 void WebIntentPickerController::CreatePicker() { | |
| 942 // If picker is non-NULL, it was set by a test. | |
| 943 if (picker_ == NULL) | |
| 944 picker_ = WebIntentPicker::Create(web_contents_, this, picker_model_.get()); | |
| 945 picker_->SetActionString(WebIntentPicker::GetDisplayStringForIntentAction( | |
| 946 picker_model_->action())); | |
| 947 web_intents::RecordPickerShow( | |
| 948 uma_bucket_, picker_model_->GetInstalledServiceCount()); | |
| 949 picker_shown_ = true; | |
| 950 } | |
| 951 | |
| 952 void WebIntentPickerController::ClosePicker() { | |
| 953 SetDialogState(kPickerHidden); | |
| 954 if (picker_) | |
| 955 picker_->Close(); | |
| 956 } | |
| 957 | |
| 958 void WebIntentPickerController::CancelDownload() { | |
| 959 if (!download_id_.IsValid()) | |
| 960 return; | |
| 961 Profile* profile = | |
| 962 Profile::FromBrowserContext(web_contents_->GetBrowserContext()); | |
| 963 content::DownloadManager* download_manager = | |
| 964 content::BrowserContext::GetDownloadManager(profile); | |
| 965 if (!download_manager) | |
| 966 return; | |
| 967 content::DownloadItem* item = | |
| 968 download_manager->GetDownload(download_id_.local()); | |
| 969 if (item) | |
| 970 item->Cancel(true); | |
| 971 download_id_ = content::DownloadId(); | |
| 972 } | |
| OLD | NEW |