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 |