| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/intents/web_intent_picker_controller.h" |
| |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/md5.h" |
| #include "base/memory/scoped_ptr.h" |
| #include "base/time.h" |
| #include "base/utf_string_conversions.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/extensions/platform_app_launcher.h" |
| #include "chrome/browser/extensions/webstore_installer.h" |
| #include "chrome/browser/favicon/favicon_service.h" |
| #include "chrome/browser/favicon/favicon_service_factory.h" |
| #include "chrome/browser/intents/cws_intents_registry_factory.h" |
| #include "chrome/browser/intents/default_web_intent_service.h" |
| #include "chrome/browser/intents/web_intents_registry_factory.h" |
| #include "chrome/browser/intents/web_intents_reporting.h" |
| #include "chrome/browser/intents/web_intents_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/tab_contents/tab_util.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/browser_navigator.h" |
| #include "chrome/browser/ui/browser_tabstrip.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/constrained_window_tab_helper.h" |
| #include "chrome/browser/ui/intents/web_intent_picker.h" |
| #include "chrome/browser/ui/intents/web_intent_picker_model.h" |
| #include "chrome/browser/ui/tab_contents/tab_contents.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/webdata/web_data_service.h" |
| #include "chrome/common/chrome_notification_types.h" |
| #include "chrome/common/url_constants.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/notification_source.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_contents_observer.h" |
| #include "content/public/browser/web_intents_dispatcher.h" |
| #include "grit/generated_resources.h" |
| #include "ipc/ipc_message.h" |
| #include "net/base/load_flags.h" |
| #include "net/url_request/url_fetcher.h" |
| #include "net/url_request/url_fetcher_delegate.h" |
| #include "skia/ext/image_operations.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/codec/png_codec.h" |
| #include "ui/gfx/favicon_size.h" |
| #include "ui/gfx/image/image.h" |
| |
| using extensions::WebstoreInstaller; |
| |
| int WebIntentPickerController::kUserDataKey; |
| |
| namespace { |
| |
| // Maximum amount of time to delay displaying dialog while waiting for data. |
| const int kMaxHiddenSetupTimeMs = 200; |
| |
| // Minimum amount of time to show waiting dialog, if it is shown. |
| const int kMinThrobberDisplayTimeMs = 800; |
| |
| |
| // Gets the favicon service for the specified profile. |
| FaviconService* GetFaviconService(Profile* profile) { |
| return FaviconServiceFactory::GetForProfile(profile, |
| Profile::EXPLICIT_ACCESS); |
| } |
| |
| // Gets the web intents registry for the specified profile. |
| WebIntentsRegistry* GetWebIntentsRegistry(Profile* profile) { |
| return WebIntentsRegistryFactory::GetForProfile(profile); |
| } |
| |
| // Gets the Chrome web store intents registry for the specified profile. |
| CWSIntentsRegistry* GetCWSIntentsRegistry(Profile* profile) { |
| return CWSIntentsRegistryFactory::GetForProfile(profile); |
| } |
| |
| // Returns the action-specific string for |action|. |
| string16 GetIntentActionString(const std::string& action) { |
| if (!action.compare(web_intents::kActionShare)) |
| return l10n_util::GetStringUTF16(IDS_WEB_INTENTS_ACTION_SHARE); |
| else if (!action.compare(web_intents::kActionEdit)) |
| return l10n_util::GetStringUTF16(IDS_WEB_INTENTS_ACTION_EDIT); |
| else if (!action.compare(web_intents::kActionView)) |
| return l10n_util::GetStringUTF16(IDS_WEB_INTENTS_ACTION_VIEW); |
| else if (!action.compare(web_intents::kActionPick)) |
| // Using generic string per UX suggestions. |
| return l10n_util::GetStringUTF16(IDS_INTENT_PICKER_CHOOSE_SERVICE); |
| else if (!action.compare(web_intents::kActionSubscribe)) |
| return l10n_util::GetStringUTF16(IDS_WEB_INTENTS_ACTION_SUBSCRIBE); |
| else if (!action.compare(web_intents::kActionSave)) |
| return l10n_util::GetStringUTF16(IDS_WEB_INTENTS_ACTION_SAVE); |
| else |
| return l10n_util::GetStringUTF16(IDS_INTENT_PICKER_CHOOSE_SERVICE); |
| } |
| |
| // Self-deleting trampoline that forwards A URLFetcher response to a callback. |
| class URLFetcherTrampoline : public net::URLFetcherDelegate { |
| public: |
| typedef base::Callback<void(const net::URLFetcher* source)> |
| ForwardingCallback; |
| |
| explicit URLFetcherTrampoline(const ForwardingCallback& callback); |
| ~URLFetcherTrampoline(); |
| |
| // net::URLFetcherDelegate implementation. |
| virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE; |
| |
| private: |
| // Fowarding callback from |OnURLFetchComplete|. |
| ForwardingCallback callback_; |
| }; |
| |
| URLFetcherTrampoline::URLFetcherTrampoline(const ForwardingCallback& callback) |
| : callback_(callback) { |
| } |
| |
| URLFetcherTrampoline::~URLFetcherTrampoline() { |
| } |
| |
| void URLFetcherTrampoline::OnURLFetchComplete( |
| const net::URLFetcher* source) { |
| DCHECK(!callback_.is_null()); |
| callback_.Run(source); |
| delete source; |
| delete this; |
| } |
| |
| class SourceWindowObserver : content::WebContentsObserver { |
| public: |
| SourceWindowObserver(content::WebContents* web_contents, |
| base::WeakPtr<WebIntentPickerController> controller) |
| : content::WebContentsObserver(web_contents), |
| controller_(controller) {} |
| virtual ~SourceWindowObserver() {} |
| |
| // Implement WebContentsObserver |
| virtual void WebContentsDestroyed(content::WebContents* web_contents) { |
| if (controller_) |
| controller_->SourceWebContentsDestroyed(web_contents); |
| delete this; |
| } |
| |
| private: |
| base::WeakPtr<WebIntentPickerController> controller_; |
| }; |
| |
| } // namespace |
| |
| // UMAReporter handles reporting Web Intents events to UMA. |
| class WebIntentPickerController::UMAReporter { |
| public: |
| |
| // Resets the service active duration timer to "now". |
| void ResetServiceActiveTimer(); |
| |
| // Records the duration of time spent using the service. Uses |reply_type| |
| // to distinguish between successful and failed service calls. |
| void RecordServiceActiveDuration(webkit_glue::WebIntentReplyType reply_type); |
| |
| private: |
| |
| // The time when the user began using the service. |
| base::TimeTicks service_start_time_; |
| }; |
| |
| void WebIntentPickerController::UMAReporter::ResetServiceActiveTimer() { |
| service_start_time_ = base::TimeTicks::Now(); |
| } |
| |
| void WebIntentPickerController::UMAReporter::RecordServiceActiveDuration( |
| webkit_glue::WebIntentReplyType reply_type) { |
| if (!service_start_time_.is_null()) { |
| web_intents::RecordServiceActiveDuration(reply_type, |
| base::TimeTicks::Now() - service_start_time_); |
| } |
| } |
| |
| WebIntentPickerController::WebIntentPickerController( |
| content::WebContents* web_contents) |
| : dialog_state_(kPickerHidden), |
| web_contents_(web_contents), |
| profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())), |
| picker_(NULL), |
| picker_model_(new WebIntentPickerModel()), |
| uma_reporter_(new UMAReporter()), |
| pending_async_count_(0), |
| pending_registry_calls_count_(0), |
| pending_cws_request_(false), |
| picker_shown_(false), |
| window_disposition_source_(NULL), |
| source_intents_dispatcher_(NULL), |
| intents_dispatcher_(NULL), |
| service_tab_(NULL), |
| ALLOW_THIS_IN_INITIALIZER_LIST(weak_ptr_factory_(this)), |
| ALLOW_THIS_IN_INITIALIZER_LIST(timer_factory_(this)), |
| ALLOW_THIS_IN_INITIALIZER_LIST(dispatcher_factory_(this)) { |
| content::NavigationController* controller = &web_contents->GetController(); |
| registrar_.Add(this, content::NOTIFICATION_LOAD_START, |
| content::Source<content::NavigationController>(controller)); |
| registrar_.Add(this, chrome::NOTIFICATION_TAB_CLOSING, |
| content::Source<content::NavigationController>(controller)); |
| #if defined(TOOLKIT_VIEWS) |
| cancelled_ = true; |
| #endif |
| } |
| |
| WebIntentPickerController::~WebIntentPickerController() { |
| } |
| |
| // TODO(gbillock): combine this with ShowDialog. |
| void WebIntentPickerController::SetIntentsDispatcher( |
| content::WebIntentsDispatcher* intents_dispatcher) { |
| // TODO(gbillock): This is to account for multiple dispatches in the same tab. |
| // That is currently not a well-handled case, and this is a band-aid. |
| dispatcher_factory_.InvalidateWeakPtrs(); |
| intents_dispatcher_ = intents_dispatcher; |
| intents_dispatcher_->RegisterReplyNotification( |
| base::Bind(&WebIntentPickerController::OnSendReturnMessage, |
| dispatcher_factory_.GetWeakPtr())); |
| |
| // Initialize the reporting bucket. |
| const webkit_glue::WebIntentData& intent = intents_dispatcher_->GetIntent(); |
| uma_bucket_ = web_intents::ToUMABucket(intent.action, intent.type); |
| } |
| |
| // TODO(smckay): rename this "StartActivity". |
| void WebIntentPickerController::ShowDialog(const string16& action, |
| const string16& type) { |
| ShowDialog(false); |
| } |
| |
| void WebIntentPickerController::ReshowDialog() { |
| ShowDialog(true); |
| } |
| |
| void WebIntentPickerController::ShowDialog(bool suppress_defaults) { |
| web_intents::RecordIntentDispatched(uma_bucket_); |
| |
| DCHECK(intents_dispatcher_); |
| |
| #if defined(TOOLKIT_VIEWS) |
| cancelled_ = true; |
| #endif |
| |
| // Only show a picker once. |
| // TODO(gbillock): There's a hole potentially admitting multiple |
| // in-flight dispatches since we don't create the picker |
| // in this method, but only after calling the registry. |
| if (picker_shown_) { |
| intents_dispatcher_->SendReplyMessage( |
| webkit_glue::WEB_INTENT_REPLY_FAILURE, |
| ASCIIToUTF16("Simultaneous intent invocation.")); |
| return; |
| } |
| |
| // TODO(binji): Figure out what to do when intents are invoked from incognito |
| // mode. |
| if (profile_->IsOffTheRecord()) { |
| intents_dispatcher_->SendReplyMessage( |
| webkit_glue::WEB_INTENT_REPLY_FAILURE, string16()); |
| return; |
| } |
| |
| picker_model_->Clear(); |
| picker_model_->set_action(intents_dispatcher_->GetIntent().action); |
| picker_model_->set_type(intents_dispatcher_->GetIntent().type); |
| |
| // If the intent is explicit, skip showing the picker. |
| const GURL& service = intents_dispatcher_->GetIntent().service; |
| // TODO(gbillock): Decide whether to honor the default suppression flag |
| // here or suppress the control for explicit intents. |
| if (service.is_valid() && !suppress_defaults) { |
| // TODO(gbillock): When we can parse pages for the intent tag, |
| // take out this requirement that explicit intents dispatch to |
| // extension urls. |
| if (!service.SchemeIs(chrome::kExtensionScheme)) { |
| intents_dispatcher_->SendReplyMessage( |
| webkit_glue::WEB_INTENT_REPLY_FAILURE, ASCIIToUTF16( |
| "Only extension urls are supported for explicit invocation")); |
| return; |
| } |
| |
| // Get services from the registry to verify a registered extension |
| // page for this action/type if it is permitted to be dispatched. (Also |
| // required to find disposition set by service.) |
| pending_async_count_++; |
| GetWebIntentsRegistry(profile_)->GetIntentServices( |
| picker_model_->action(), picker_model_->type(), |
| base::Bind( |
| &WebIntentPickerController:: |
| OnWebIntentServicesAvailableForExplicitIntent, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| // As soon as the dialog is requested, block all input events |
| // on the original tab. |
| TabContents* tab_contents = TabContents::FromWebContents(web_contents_); |
| tab_contents->constrained_window_tab_helper()->BlockTabContent(true); |
| SetDialogState(kPickerSetup); |
| |
| pending_async_count_++; |
| pending_registry_calls_count_++; |
| GetWebIntentsRegistry(profile_)->GetIntentServices( |
| picker_model_->action(), picker_model_->type(), |
| base::Bind(&WebIntentPickerController::OnWebIntentServicesAvailable, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| GURL invoking_url = web_contents_->GetURL(); |
| if (invoking_url.is_valid() && !suppress_defaults) { |
| pending_async_count_++; |
| pending_registry_calls_count_++; |
| GetWebIntentsRegistry(profile_)->GetDefaultIntentService( |
| picker_model_->action(), picker_model_->type(), invoking_url, |
| base::Bind(&WebIntentPickerController::OnWebIntentDefaultsAvailable, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| pending_cws_request_ = true; |
| pending_async_count_++; |
| GetCWSIntentsRegistry(profile_)->GetIntentServices( |
| picker_model_->action(), picker_model_->type(), |
| base::Bind(&WebIntentPickerController::OnCWSIntentServicesAvailable, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void WebIntentPickerController::Observe( |
| int type, |
| const content::NotificationSource& source, |
| const content::NotificationDetails& details) { |
| DCHECK(type == content::NOTIFICATION_LOAD_START || |
| type == chrome::NOTIFICATION_TAB_CLOSING); |
| ClosePicker(); |
| } |
| |
| void WebIntentPickerController::OnServiceChosen( |
| const GURL& url, |
| webkit_glue::WebIntentServiceData::Disposition disposition) { |
| web_intents::RecordServiceInvoke(uma_bucket_); |
| uma_reporter_->ResetServiceActiveTimer(); |
| ExtensionService* service = profile_->GetExtensionService(); |
| DCHECK(service); |
| |
| #if defined(TOOLKIT_VIEWS) |
| cancelled_ = false; |
| #endif |
| |
| // Set the default here. Activating the intent resets the picker model. |
| // TODO(gbillock): we should perhaps couple the model to the dispatcher so |
| // we can re-activate the model on use-another-service. |
| SetDefaultServiceForSelection(url); |
| |
| const extensions::Extension* extension = service->GetInstalledApp(url); |
| |
| // TODO(smckay): this basically smells like another disposition. |
| if (extension && extension->is_platform_app()) { |
| extensions::LaunchPlatformAppWithWebIntent(profile_, |
| extension, intents_dispatcher_, web_contents_); |
| ClosePicker(); |
| return; |
| } |
| |
| switch (disposition) { |
| case webkit_glue::WebIntentServiceData::DISPOSITION_INLINE: |
| // Set the model to inline disposition. It will notify the picker which |
| // will respond (via OnInlineDispositionWebContentsCreated) with the |
| // WebContents to dispatch the intent to. |
| picker_model_->SetInlineDisposition(url); |
| break; |
| |
| case webkit_glue::WebIntentServiceData::DISPOSITION_WINDOW: { |
| TabContents* contents = chrome::TabContentsFactory( |
| profile_, |
| tab_util::GetSiteInstanceForNewTab(profile_, url), |
| MSG_ROUTING_NONE, NULL); |
| |
| // Let the controller for the target TabContents know that it is hosting a |
| // web intents service. |
| WebIntentPickerController::FromWebContents(contents->web_contents())-> |
| SetWindowDispositionSource(web_contents_, intents_dispatcher_); |
| |
| intents_dispatcher_->DispatchIntent(contents->web_contents()); |
| service_tab_ = contents->web_contents(); |
| |
| // This call performs all the tab strip manipulation, notifications, etc. |
| // Since we're passing in a target_contents, it assumes that we will |
| // navigate the page ourselves, though. |
| chrome::NavigateParams params(profile_, url, |
| content::PAGE_TRANSITION_LINK); |
| params.target_contents = contents; |
| params.disposition = NEW_FOREGROUND_TAB; |
| params.tabstrip_add_types = TabStripModel::ADD_INHERIT_GROUP; |
| chrome::Navigate(¶ms); |
| |
| service_tab_->GetController().LoadURL( |
| url, content::Referrer(), |
| content::PAGE_TRANSITION_AUTO_BOOKMARK, std::string()); |
| |
| ClosePicker(); |
| break; |
| } |
| |
| default: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| // Take the MD5 digest of the origins of picker options |
| // and record it as the default context string. |
| int64 WebIntentPickerController::DigestServices() { |
| // The context in which the default is registered is all |
| // the installed services represented in the picker (represented |
| // by their site origins). |
| std::vector<std::string> context_urls; |
| for (size_t i = 0; i < picker_model_->GetInstalledServiceCount(); ++i) { |
| const GURL& url = picker_model_->GetInstalledServiceAt(i).url; |
| if (url.SchemeIs(chrome::kExtensionScheme)) |
| context_urls.push_back(url.host()); // this is the extension ID |
| else |
| context_urls.push_back(url.GetOrigin().spec()); |
| } |
| std::sort(context_urls.begin(), context_urls.end()); |
| |
| base::MD5Context md5_context; |
| base::MD5Init(&md5_context); |
| for (size_t i = 0; i < context_urls.size(); ++i) |
| base::MD5Update(&md5_context, context_urls[i]); |
| base::MD5Digest digest; |
| base::MD5Final(&digest, &md5_context); |
| int64 hash = 0; |
| COMPILE_ASSERT(sizeof(base::MD5Digest) > sizeof(int64), |
| int64_size_greater_than_md5_buffer); |
| memcpy(&hash, &digest, sizeof(int64)); |
| |
| return hash; |
| } |
| |
| void WebIntentPickerController::SetDefaultServiceForSelection(const GURL& url) { |
| int64 service_hash = DigestServices(); |
| DCHECK(picker_model_.get()); |
| if (url == picker_model_->default_service_url() && |
| service_hash == picker_model_->default_service_hash()) { |
| return; |
| } |
| |
| DefaultWebIntentService record; |
| record.action = picker_model_->action(); |
| record.type = picker_model_->type(); |
| record.service_url = url.spec(); |
| record.suppression = service_hash; |
| record.user_date = static_cast<int>(floor(base::Time::Now().ToDoubleT())); |
| GetWebIntentsRegistry(profile_)->RegisterDefaultIntentService(record); |
| } |
| |
| void WebIntentPickerController::OnInlineDispositionWebContentsCreated( |
| content::WebContents* web_contents) { |
| if (web_contents) |
| intents_dispatcher_->DispatchIntent(web_contents); |
| } |
| |
| void WebIntentPickerController::OnExtensionInstallRequested( |
| const std::string& id) { |
| scoped_ptr<WebstoreInstaller::Approval> approval( |
| WebstoreInstaller::Approval::CreateWithInstallPrompt(profile_)); |
| |
| scoped_refptr<WebstoreInstaller> installer = new WebstoreInstaller( |
| profile_, this, |
| &web_contents_->GetController(), id, |
| approval.Pass(), WebstoreInstaller::FLAG_INLINE_INSTALL); |
| |
| pending_async_count_++; |
| installer->Start(); |
| } |
| |
| void WebIntentPickerController::OnExtensionLinkClicked( |
| const std::string& id, |
| WindowOpenDisposition disposition) { |
| // Navigate from source tab. |
| GURL extension_url(extension_urls::GetWebstoreItemDetailURLPrefix() + id); |
| chrome::NavigateParams params(profile_, extension_url, |
| content::PAGE_TRANSITION_LINK); |
| params.disposition = |
| (disposition == CURRENT_TAB) ? NEW_FOREGROUND_TAB : disposition; |
| chrome::Navigate(¶ms); |
| } |
| |
| void WebIntentPickerController::OnSuggestionsLinkClicked( |
| WindowOpenDisposition disposition) { |
| // Navigate from source tab. |
| GURL query_url = extension_urls::GetWebstoreIntentQueryURL( |
| UTF16ToUTF8(picker_model_->action()), |
| UTF16ToUTF8(picker_model_->type())); |
| chrome::NavigateParams params(profile_, query_url, |
| content::PAGE_TRANSITION_LINK); |
| params.disposition = |
| (disposition == CURRENT_TAB) ? NEW_FOREGROUND_TAB : disposition; |
| chrome::Navigate(¶ms); |
| } |
| |
| void WebIntentPickerController::OnUserCancelledPickerDialog() { |
| if (!intents_dispatcher_) |
| return; |
| |
| intents_dispatcher_->SendReplyMessage( |
| webkit_glue::WEB_INTENT_PICKER_CANCELLED, string16()); |
| web_intents::RecordPickerCancel(uma_bucket_); |
| |
| ClosePicker(); |
| } |
| |
| void WebIntentPickerController::OnChooseAnotherService() { |
| DCHECK(intents_dispatcher_); |
| web_intents::RecordChooseAnotherService(uma_bucket_); |
| intents_dispatcher_->ResetDispatch(); |
| } |
| |
| void WebIntentPickerController::OnClosing() { |
| SetDialogState(kPickerHidden); |
| picker_ = NULL; |
| #if defined(TOOLKIT_VIEWS) |
| if (cancelled_) |
| OnUserCancelledPickerDialog(); |
| #endif |
| } |
| |
| void WebIntentPickerController::OnExtensionInstallSuccess( |
| const std::string& extension_id) { |
| // OnExtensionInstallSuccess is called via NotificationService::Notify before |
| // the extension is added to the ExtensionService. Dispatch via PostTask to |
| // allow ExtensionService to update. |
| MessageLoop::current()->PostTask( |
| FROM_HERE, |
| base::Bind( |
| &WebIntentPickerController::DispatchToInstalledExtension, |
| base::Unretained(this), |
| extension_id)); |
| } |
| |
| void WebIntentPickerController::DispatchToInstalledExtension( |
| const std::string& extension_id) { |
| web_intents::RecordCWSExtensionInstalled(uma_bucket_); |
| picker_->OnExtensionInstallSuccess(extension_id); |
| WebIntentsRegistry::IntentServiceList services; |
| GetWebIntentsRegistry(profile_)->GetIntentServicesForExtensionFilter( |
| picker_model_->action(), picker_model_->type(), |
| extension_id, |
| &services); |
| |
| // Extension must be registered with registry by now. |
| DCHECK(services.size() > 0); |
| |
| // TODO(binji): We're going to need to disambiguate if there are multiple |
| // services. For now, just choose the first. |
| const webkit_glue::WebIntentServiceData& service_data = services[0]; |
| |
| picker_model_->AddInstalledService( |
| service_data.title, service_data.service_url, |
| service_data.disposition); |
| OnServiceChosen(service_data.service_url, service_data.disposition); |
| AsyncOperationFinished(); |
| } |
| |
| void WebIntentPickerController::OnExtensionInstallFailure( |
| const std::string& id, |
| const std::string& error) { |
| picker_->OnExtensionInstallFailure(id); |
| AsyncOperationFinished(); |
| } |
| |
| void WebIntentPickerController::OnSendReturnMessage( |
| webkit_glue::WebIntentReplyType reply_type) { |
| ClosePicker(); |
| uma_reporter_->RecordServiceActiveDuration(reply_type); |
| |
| if (service_tab_ && |
| reply_type != webkit_glue::WEB_INTENT_SERVICE_CONTENTS_CLOSED) { |
| Browser* browser = browser::FindBrowserWithWebContents(service_tab_); |
| if (browser) { |
| int index = browser->tab_strip_model()->GetIndexOfWebContents( |
| service_tab_); |
| browser->tab_strip_model()->CloseTabContentsAt( |
| index, TabStripModel::CLOSE_CREATE_HISTORICAL_TAB); |
| |
| // Activate source tab. |
| Browser* source_browser = |
| browser::FindBrowserWithWebContents(web_contents_); |
| if (source_browser) { |
| int source_index = source_browser->tab_strip_model()-> |
| GetIndexOfWebContents(web_contents_); |
| chrome::ActivateTabAt(source_browser, source_index, false); |
| } |
| } |
| service_tab_ = NULL; |
| } |
| |
| intents_dispatcher_ = NULL; |
| } |
| |
| void WebIntentPickerController::AddServiceToModel( |
| const webkit_glue::WebIntentServiceData& service) { |
| FaviconService* favicon_service = GetFaviconService(profile_); |
| |
| picker_model_->AddInstalledService( |
| service.title, |
| service.service_url, |
| service.disposition); |
| |
| pending_async_count_++; |
| FaviconService::Handle handle = favicon_service->GetFaviconImageForURL( |
| FaviconService::FaviconForURLParams( |
| profile_, |
| service.service_url, |
| history::FAVICON, |
| gfx::kFaviconSize, |
| &favicon_consumer_), |
| base::Bind( |
| &WebIntentPickerController::OnFaviconDataAvailable, |
| weak_ptr_factory_.GetWeakPtr())); |
| favicon_consumer_.SetClientData( |
| favicon_service, handle, picker_model_->GetInstalledServiceCount() - 1); |
| } |
| |
| void WebIntentPickerController::OnWebIntentServicesAvailable( |
| const std::vector<webkit_glue::WebIntentServiceData>& services) { |
| for (size_t i = 0; i < services.size(); ++i) |
| AddServiceToModel(services[i]); |
| |
| RegistryCallsCompleted(); |
| AsyncOperationFinished(); |
| } |
| |
| void WebIntentPickerController::OnWebIntentServicesAvailableForExplicitIntent( |
| const std::vector<webkit_glue::WebIntentServiceData>& services) { |
| DCHECK(intents_dispatcher_); |
| DCHECK(intents_dispatcher_->GetIntent().service.is_valid()); |
| for (size_t i = 0; i < services.size(); ++i) { |
| if (services[i].service_url != intents_dispatcher_->GetIntent().service) |
| continue; |
| |
| AddServiceToModel(services[i]); |
| |
| // BUG? This should a) not use (i) |
| InvokeService(picker_model_->GetInstalledServiceAt(i)); |
| AsyncOperationFinished(); |
| return; |
| } |
| |
| // No acceptable extension. The intent cannot be dispatched. |
| intents_dispatcher_->SendReplyMessage( |
| webkit_glue::WEB_INTENT_REPLY_FAILURE, ASCIIToUTF16( |
| "Explicit extension URL is not available.")); |
| |
| AsyncOperationFinished(); |
| } |
| |
| void WebIntentPickerController::OnWebIntentDefaultsAvailable( |
| const DefaultWebIntentService& default_service) { |
| if (!default_service.service_url.empty()) { |
| picker_model_->set_default_service_url(GURL(default_service.service_url)); |
| picker_model_->set_default_service_hash(default_service.suppression); |
| } |
| |
| RegistryCallsCompleted(); |
| AsyncOperationFinished(); |
| } |
| |
| void WebIntentPickerController::RegistryCallsCompleted() { |
| pending_registry_calls_count_--; |
| if (pending_registry_calls_count_ != 0) return; |
| |
| if (picker_model_->default_service_url().is_valid() && |
| picker_model_->default_service_hash() == DigestServices()) { |
| // If there's a default service, dispatch to it immediately |
| // without showing the picker. |
| const WebIntentPickerModel::InstalledService* default_service = |
| picker_model_->GetInstalledServiceWithURL( |
| GURL(picker_model_->default_service_url())); |
| if (default_service != NULL) { |
| InvokeService(*default_service); |
| return; |
| } |
| } |
| |
| OnPickerEvent(kPickerEventRegistryDataComplete); |
| OnIntentDataArrived(); |
| } |
| |
| void WebIntentPickerController::OnFaviconDataAvailable( |
| FaviconService::Handle handle, |
| const history::FaviconImageResult& image_result) { |
| size_t index = favicon_consumer_.GetClientDataForCurrentRequest(); |
| if (!image_result.image.IsEmpty()) { |
| picker_model_->UpdateFaviconAt(index, image_result.image); |
| return; |
| } |
| |
| AsyncOperationFinished(); |
| } |
| |
| void WebIntentPickerController::OnCWSIntentServicesAvailable( |
| const CWSIntentsRegistry::IntentExtensionList& extensions) { |
| ExtensionServiceInterface* extension_service = |
| profile_->GetExtensionService(); |
| |
| std::vector<WebIntentPickerModel::SuggestedExtension> suggestions; |
| for (size_t i = 0; i < extensions.size(); ++i) { |
| const CWSIntentsRegistry::IntentExtensionInfo& info = extensions[i]; |
| |
| // Do not include suggestions for already installed extensions. |
| if (extension_service->GetExtensionById(UTF16ToUTF8(info.id), |
| true)) { |
| continue; |
| } |
| |
| suggestions.push_back(WebIntentPickerModel::SuggestedExtension( |
| info.name, info.id, info.average_rating)); |
| |
| pending_async_count_++; |
| net::URLFetcher* icon_url_fetcher = net::URLFetcher::Create( |
| 0, |
| info.icon_url, |
| net::URLFetcher::GET, |
| new URLFetcherTrampoline( |
| base::Bind( |
| &WebIntentPickerController::OnExtensionIconURLFetchComplete, |
| weak_ptr_factory_.GetWeakPtr(), info.id))); |
| |
| icon_url_fetcher->SetLoadFlags( |
| net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES); |
| icon_url_fetcher->SetRequestContext(profile_->GetRequestContext()); |
| icon_url_fetcher->Start(); |
| } |
| |
| picker_model_->AddSuggestedExtensions(suggestions); |
| |
| AsyncOperationFinished(); |
| pending_cws_request_ = false; |
| OnIntentDataArrived(); |
| } |
| |
| void WebIntentPickerController::OnExtensionIconURLFetchComplete( |
| const string16& extension_id, const net::URLFetcher* source) { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| if (source->GetResponseCode() != 200) { |
| AsyncOperationFinished(); |
| return; |
| } |
| |
| scoped_ptr<std::string> response(new std::string); |
| if (!source->GetResponseAsString(response.get())) { |
| AsyncOperationFinished(); |
| return; |
| } |
| |
| // I'd like to have the worker thread post a task directly to the UI thread |
| // to call OnExtensionIcon[Un]Available, but this doesn't work: To do so |
| // would require DecodeExtensionIconAndResize to be a member function (so it |
| // has access to |this|) but a weak pointer cannot be dereferenced on a |
| // thread other than the thread where the WeakPtrFactory was created. Since |
| // the stored |this| pointer is weak, DecodeExtensionIconAndResize asserts |
| // before it even starts. |
| // |
| // Instead, I package up the callbacks that I want the worker thread to call, |
| // and make DecodeExtensionIconAndResize static. The stored weak |this| |
| // pointers are not dereferenced until invocation (on the UI thread). |
| ExtensionIconAvailableCallback available_callback = |
| base::Bind( |
| &WebIntentPickerController::OnExtensionIconAvailable, |
| weak_ptr_factory_.GetWeakPtr(), |
| extension_id); |
| base::Closure unavailable_callback = |
| base::Bind( |
| &WebIntentPickerController::OnExtensionIconUnavailable, |
| weak_ptr_factory_.GetWeakPtr(), |
| extension_id); |
| |
| // Decode PNG and resize on worker thread. |
| content::BrowserThread::PostBlockingPoolTask( |
| FROM_HERE, |
| base::Bind(&DecodeExtensionIconAndResize, |
| base::Passed(&response), |
| available_callback, |
| unavailable_callback)); |
| } |
| |
| void WebIntentPickerController::OnIntentDataArrived() { |
| DCHECK(picker_model_.get()); |
| |
| if (!pending_cws_request_ && |
| pending_registry_calls_count_ == 0) |
| OnPickerEvent(kPickerEventAsyncDataComplete); |
| } |
| |
| void WebIntentPickerController::Reset() { |
| // Abandon all callbacks. |
| weak_ptr_factory_.InvalidateWeakPtrs(); |
| timer_factory_.InvalidateWeakPtrs(); |
| |
| // Reset state associated with callbacks. |
| pending_async_count_ = 0; |
| pending_registry_calls_count_ = 0; |
| pending_cws_request_ = false; |
| |
| // Reset picker. |
| picker_model_.reset(new WebIntentPickerModel()); |
| picker_shown_ = false; |
| |
| DCHECK(web_contents_); |
| TabContents* tab_contents = TabContents::FromWebContents(web_contents_); |
| tab_contents->constrained_window_tab_helper()->BlockTabContent(false); |
| } |
| |
| // static |
| void WebIntentPickerController::DecodeExtensionIconAndResize( |
| scoped_ptr<std::string> icon_response, |
| const ExtensionIconAvailableCallback& callback, |
| const base::Closure& unavailable_callback) { |
| SkBitmap icon_bitmap; |
| if (gfx::PNGCodec::Decode( |
| reinterpret_cast<const unsigned char*>(icon_response->data()), |
| icon_response->length(), |
| &icon_bitmap)) { |
| SkBitmap resized_icon = skia::ImageOperations::Resize( |
| icon_bitmap, |
| skia::ImageOperations::RESIZE_BEST, |
| gfx::kFaviconSize, gfx::kFaviconSize); |
| gfx::Image icon_image(resized_icon); |
| |
| content::BrowserThread::PostTask( |
| content::BrowserThread::UI, |
| FROM_HERE, |
| base::Bind(callback, icon_image)); |
| } else { |
| content::BrowserThread::PostTask( |
| content::BrowserThread::UI, |
| FROM_HERE, |
| unavailable_callback); |
| } |
| } |
| |
| void WebIntentPickerController::OnExtensionIconAvailable( |
| const string16& extension_id, |
| const gfx::Image& icon_image) { |
| picker_model_->SetSuggestedExtensionIconWithId(extension_id, icon_image); |
| AsyncOperationFinished(); |
| } |
| |
| void WebIntentPickerController::OnExtensionIconUnavailable( |
| const string16& extension_id) { |
| AsyncOperationFinished(); |
| } |
| |
| void WebIntentPickerController::SetWindowDispositionSource( |
| content::WebContents* source, |
| content::WebIntentsDispatcher* dispatcher) { |
| DCHECK(source); |
| DCHECK(dispatcher); |
| window_disposition_source_ = source; |
| if (window_disposition_source_) { |
| // This object is self-deleting when the source WebContents is destroyed. |
| new SourceWindowObserver(window_disposition_source_, |
| weak_ptr_factory_.GetWeakPtr()); |
| } |
| |
| source_intents_dispatcher_ = dispatcher; |
| if (dispatcher) { |
| dispatcher->RegisterReplyNotification( |
| base::Bind(&WebIntentPickerController::SourceDispatcherReplied, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void WebIntentPickerController::SourceWebContentsDestroyed( |
| content::WebContents* source) { |
| window_disposition_source_ = NULL; |
| // TODO(gbillock): redraw location bar to kill button |
| } |
| |
| void WebIntentPickerController::SourceDispatcherReplied( |
| webkit_glue::WebIntentReplyType reply_type) { |
| source_intents_dispatcher_ = NULL; |
| // TODO(gbillock): redraw location bar to kill button |
| } |
| |
| bool WebIntentPickerController::ShowLocationBarPickerTool() { |
| return window_disposition_source_ || source_intents_dispatcher_; |
| } |
| |
| void WebIntentPickerController::OnPickerEvent(WebIntentPickerEvent event) { |
| switch (event) { |
| case kPickerEventHiddenSetupTimeout: |
| DCHECK(dialog_state_ == kPickerSetup); |
| SetDialogState(kPickerWaiting); |
| break; |
| |
| case kPickerEventMaxWaitTimeExceeded: |
| DCHECK(dialog_state_ == kPickerWaiting); |
| |
| // If registry data is complete, go to main dialog. Otherwise, wait. |
| if (pending_registry_calls_count_ == 0) |
| SetDialogState(kPickerMain); |
| else |
| SetDialogState(kPickerWaitLong); |
| break; |
| |
| case kPickerEventRegistryDataComplete: |
| DCHECK(dialog_state_ == kPickerSetup || |
| dialog_state_ == kPickerWaiting || |
| dialog_state_ == kPickerWaitLong); |
| |
| // If minimum wait dialog time is exceeded, display main dialog. |
| // Either way, we don't do a thing. |
| break; |
| |
| case kPickerEventAsyncDataComplete: |
| DCHECK(dialog_state_ == kPickerSetup || |
| dialog_state_ == kPickerWaiting || |
| dialog_state_ == kPickerWaitLong || |
| dialog_state_ == kPickerInline); |
| |
| // In setup state, transition to main dialog. In waiting state, let |
| // timer expire. |
| if (dialog_state_ == kPickerSetup) |
| SetDialogState(kPickerMain); |
| break; |
| |
| default: |
| NOTREACHED(); |
| break; |
| } |
| } |
| |
| void WebIntentPickerController::LocationBarPickerToolClicked() { |
| DCHECK(web_contents_); |
| if (window_disposition_source_ && source_intents_dispatcher_) { |
| Browser* service_browser = |
| browser::FindBrowserWithWebContents(web_contents_); |
| if (!service_browser) return; |
| |
| TabContents* client_tab = |
| TabContents::FromWebContents(window_disposition_source_); |
| Browser* client_browser = |
| browser::FindBrowserWithWebContents(window_disposition_source_); |
| if (!client_browser || !client_tab) return; |
| int client_index = |
| client_browser->tab_strip_model()->GetIndexOfTabContents(client_tab); |
| DCHECK(client_index != TabStripModel::kNoTab); |
| |
| source_intents_dispatcher_->ResetDispatch(); |
| |
| chrome::CloseWebContents(service_browser, web_contents_); |
| |
| // Re-open the other tab and activate the picker. |
| client_browser->window()->Activate(); |
| client_browser->tab_strip_model()->ActivateTabAt(client_index, true); |
| // The picker has been Reset() when the new tab is created; need to fully |
| // reload. |
| WebIntentPickerController::FromWebContents(window_disposition_source_)-> |
| ReshowDialog(); |
| } |
| // TODO(gbillock): figure out what we ought to do in this case. Probably |
| // nothing? Refresh the location bar? |
| } |
| |
| void WebIntentPickerController::AsyncOperationFinished() { |
| DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); |
| if (--pending_async_count_ == 0) { |
| if (picker_) |
| picker_->OnPendingAsyncCompleted(); |
| } |
| } |
| |
| void WebIntentPickerController::InvokeService( |
| const WebIntentPickerModel::InstalledService& service) { |
| if (service.disposition == |
| webkit_glue::WebIntentServiceData::DISPOSITION_INLINE) { |
| SetDialogState(kPickerInline); |
| } |
| OnServiceChosen(service.url, service.disposition); |
| } |
| |
| void WebIntentPickerController::SetDialogState(WebIntentPickerState state) { |
| // Ignore events that don't change state. |
| if (state == dialog_state_) |
| return; |
| |
| // Any pending timers are abandoned on state changes. |
| timer_factory_.InvalidateWeakPtrs(); |
| |
| switch (state) { |
| case kPickerSetup: |
| DCHECK_EQ(dialog_state_, kPickerHidden); |
| // Post timer CWS pending |
| MessageLoop::current()->PostDelayedTask(FROM_HERE, |
| base::Bind(&WebIntentPickerController::OnPickerEvent, |
| timer_factory_.GetWeakPtr(), |
| kPickerEventHiddenSetupTimeout), |
| base::TimeDelta::FromMilliseconds(kMaxHiddenSetupTimeMs)); |
| break; |
| |
| case kPickerWaiting: |
| DCHECK_EQ(dialog_state_, kPickerSetup); |
| // Waiting dialog can be dismissed after minimum wait time. |
| MessageLoop::current()->PostDelayedTask(FROM_HERE, |
| base::Bind(&WebIntentPickerController::OnPickerEvent, |
| timer_factory_.GetWeakPtr(), |
| kPickerEventMaxWaitTimeExceeded), |
| base::TimeDelta::FromMilliseconds(kMinThrobberDisplayTimeMs)); |
| break; |
| |
| case kPickerWaitLong: |
| DCHECK_EQ(dialog_state_, kPickerWaiting); |
| break; |
| |
| case kPickerInline: |
| // Intentional fall-through. |
| case kPickerMain: |
| // No DCHECK - main state can be reached from any state. |
| // Ready to display data. |
| picker_model_->SetWaitingForSuggestions(false); |
| break; |
| |
| case kPickerHidden: |
| Reset(); |
| break; |
| |
| default: |
| NOTREACHED(); |
| break; |
| |
| } |
| |
| dialog_state_ = state; |
| |
| // Create picker dialog when changing away from hidden state. |
| if (dialog_state_ != kPickerHidden && dialog_state_ != kPickerSetup) |
| CreatePicker(); |
| } |
| |
| void WebIntentPickerController::CreatePicker() { |
| // If picker is non-NULL, it was set by a test. |
| if (picker_ == NULL) |
| picker_ = WebIntentPicker::Create(web_contents_, this, picker_model_.get()); |
| picker_->SetActionString(GetIntentActionString( |
| UTF16ToUTF8(picker_model_->action()))); |
| web_intents::RecordPickerShow( |
| uma_bucket_, picker_model_->GetInstalledServiceCount()); |
| picker_shown_ = true; |
| } |
| |
| void WebIntentPickerController::ClosePicker() { |
| SetDialogState(kPickerHidden); |
| if (picker_) |
| picker_->Close(); |
| } |