| // Copyright 2015 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/webui/media_router/media_router_ui.h" |
| |
| #include <string> |
| |
| #include "base/guid.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/media/router/create_presentation_connection_request.h" |
| #include "chrome/browser/media/router/issue.h" |
| #include "chrome/browser/media/router/issues_observer.h" |
| #include "chrome/browser/media/router/media_route.h" |
| #include "chrome/browser/media/router/media_router.h" |
| #include "chrome/browser/media/router/media_router_factory.h" |
| #include "chrome/browser/media/router/media_router_mojo_impl.h" |
| #include "chrome/browser/media/router/media_routes_observer.h" |
| #include "chrome/browser/media/router/media_sink.h" |
| #include "chrome/browser/media/router/media_sinks_observer.h" |
| #include "chrome/browser/media/router/media_source.h" |
| #include "chrome/browser/media/router/media_source_helper.h" |
| #include "chrome/browser/media/router/presentation_service_delegate_impl.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/sessions/session_tab_helper.h" |
| #include "chrome/browser/ui/webui/media_router/media_router_localized_strings_provider.h" |
| #include "chrome/browser/ui/webui/media_router/media_router_resources_provider.h" |
| #include "chrome/browser/ui/webui/media_router/media_router_webui_message_handler.h" |
| #include "chrome/common/url_constants.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/browser/web_ui.h" |
| #include "content/public/browser/web_ui_data_source.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/web_dialogs/web_dialog_delegate.h" |
| |
| namespace media_router { |
| |
| namespace { |
| |
| // The amount of time to wait for a response when creating a new route. |
| const int kCreateRouteTimeoutSeconds = 20; |
| |
| std::string GetHostFromURL(const GURL& gurl) { |
| if (gurl.is_empty()) |
| return std::string(); |
| std::string host = gurl.host(); |
| if (base::StartsWith(host, "www.", base::CompareCase::INSENSITIVE_ASCII)) |
| host = host.substr(4); |
| return host; |
| } |
| |
| std::string GetTruncatedHostFromURL(const GURL& gurl) { |
| std::string host = GetHostFromURL(gurl); |
| |
| const std::string truncated = |
| net::registry_controlled_domains::GetDomainAndRegistry( |
| host, |
| net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES); |
| // The truncation will be empty in some scenarios (e.g. host is |
| // simply an IP address). Fail gracefully. |
| if (truncated.empty()) |
| return host; |
| return truncated; |
| } |
| |
| } // namespace |
| |
| // This class calls to refresh the UI when the highest priority issue is |
| // updated. |
| class MediaRouterUI::UIIssuesObserver : public IssuesObserver { |
| public: |
| UIIssuesObserver(MediaRouter* router, MediaRouterUI* ui) |
| : IssuesObserver(router), ui_(ui) { |
| DCHECK(ui); |
| } |
| |
| ~UIIssuesObserver() override {} |
| |
| // IssuesObserver implementation. |
| void OnIssueUpdated(const Issue* issue) override { ui_->SetIssue(issue); } |
| |
| private: |
| // Reference back to the owning MediaRouterUI instance. |
| MediaRouterUI* ui_; |
| |
| DISALLOW_COPY_AND_ASSIGN(UIIssuesObserver); |
| }; |
| |
| MediaRouterUI::UIMediaRoutesObserver::UIMediaRoutesObserver( |
| MediaRouter* router, |
| const RoutesUpdatedCallback& callback) |
| : MediaRoutesObserver(router), callback_(callback) { |
| DCHECK(!callback_.is_null()); |
| } |
| |
| MediaRouterUI::UIMediaRoutesObserver::~UIMediaRoutesObserver() {} |
| |
| void MediaRouterUI::UIMediaRoutesObserver::OnRoutesUpdated( |
| const std::vector<MediaRoute>& routes) { |
| std::vector<MediaRoute> routes_for_display; |
| for (const MediaRoute& route : routes) { |
| if (route.for_display()) { |
| #ifndef NDEBUG |
| for (const MediaRoute& existing_route : routes_for_display) { |
| if (existing_route.media_sink_id() == route.media_sink_id()) { |
| DVLOG(2) << "Received another route for display with the same sink" |
| << " id as an existing route. " |
| << route.media_route_id() << " has the same sink id as " |
| << existing_route.media_sink_id() << "."; |
| } |
| } |
| #endif |
| routes_for_display.push_back(route); |
| } |
| } |
| |
| callback_.Run(routes_for_display); |
| } |
| |
| MediaRouterUI::MediaRouterUI(content::WebUI* web_ui) |
| : ConstrainedWebDialogUI(web_ui), |
| handler_(new MediaRouterWebUIMessageHandler(this)), |
| ui_initialized_(false), |
| requesting_route_for_default_source_(false), |
| current_route_request_id_(-1), |
| route_request_counter_(0), |
| initiator_(nullptr), |
| router_(nullptr), |
| weak_factory_(this) { |
| // Create a WebUIDataSource containing the chrome://media-router page's |
| // content. |
| scoped_ptr<content::WebUIDataSource> html_source( |
| content::WebUIDataSource::Create(chrome::kChromeUIMediaRouterHost)); |
| |
| content::WebContents* wc = web_ui->GetWebContents(); |
| DCHECK(wc); |
| |
| router_ = static_cast<MediaRouterMojoImpl*>( |
| MediaRouterFactory::GetApiForBrowserContext(wc->GetBrowserContext())); |
| |
| // Allows UI to load extensionview. |
| // TODO(haibinlu): limit object-src to current extension once crbug/514866 |
| // is fixed. |
| html_source->OverrideContentSecurityPolicyObjectSrc("object-src *;"); |
| |
| AddLocalizedStrings(html_source.get()); |
| AddMediaRouterUIResources(html_source.get()); |
| // Ownership of |html_source| is transferred to the BrowserContext. |
| content::WebUIDataSource::Add(Profile::FromWebUI(web_ui), |
| html_source.release()); |
| |
| // Ownership of |handler_| is transferred to |web_ui|. |
| web_ui->AddMessageHandler(handler_); |
| } |
| |
| MediaRouterUI::~MediaRouterUI() { |
| if (issues_observer_) |
| issues_observer_->UnregisterObserver(); |
| |
| if (query_result_manager_.get()) |
| query_result_manager_->RemoveObserver(this); |
| if (presentation_service_delegate_.get()) |
| presentation_service_delegate_->RemoveDefaultPresentationRequestObserver( |
| this); |
| // If |create_session_request_| still exists, then it means presentation route |
| // request was never attempted. |
| if (create_session_request_) { |
| create_session_request_->InvokeErrorCallback(content::PresentationError( |
| content::PRESENTATION_ERROR_SESSION_REQUEST_CANCELLED, |
| "Dialog closed.")); |
| } |
| } |
| |
| void MediaRouterUI::InitWithDefaultMediaSource( |
| const base::WeakPtr<PresentationServiceDelegateImpl>& delegate) { |
| DCHECK(delegate); |
| DCHECK(!presentation_service_delegate_); |
| DCHECK(!query_result_manager_.get()); |
| |
| presentation_service_delegate_ = delegate; |
| presentation_service_delegate_->AddDefaultPresentationRequestObserver(this); |
| InitCommon(presentation_service_delegate_->web_contents()); |
| if (presentation_service_delegate_->HasDefaultPresentationRequest()) { |
| OnDefaultPresentationChanged( |
| presentation_service_delegate_->GetDefaultPresentationRequest()); |
| } |
| } |
| |
| void MediaRouterUI::InitWithPresentationSessionRequest( |
| content::WebContents* initiator, |
| const base::WeakPtr<PresentationServiceDelegateImpl>& delegate, |
| scoped_ptr<CreatePresentationConnectionRequest> create_session_request) { |
| DCHECK(initiator); |
| DCHECK(create_session_request); |
| DCHECK(!create_session_request_); |
| DCHECK(!query_result_manager_); |
| |
| create_session_request_ = create_session_request.Pass(); |
| presentation_service_delegate_ = delegate; |
| InitCommon(initiator); |
| OnDefaultPresentationChanged(create_session_request_->presentation_request()); |
| } |
| |
| void MediaRouterUI::InitCommon(content::WebContents* initiator) { |
| DCHECK(initiator); |
| DCHECK(router_); |
| |
| // Register for MediaRoute updates. |
| routes_observer_.reset(new UIMediaRoutesObserver( |
| router_, |
| base::Bind(&MediaRouterUI::OnRoutesUpdated, base::Unretained(this)))); |
| |
| query_result_manager_.reset(new QueryResultManager(router_)); |
| query_result_manager_->AddObserver(this); |
| |
| // These modes are always available. |
| query_result_manager_->StartSinksQuery( |
| MediaCastMode::DESKTOP_MIRROR, MediaSourceForDesktop()); |
| initiator_ = initiator; |
| MediaSource mirroring_source( |
| MediaSourceForTab(SessionTabHelper::IdForTab(initiator))); |
| query_result_manager_->StartSinksQuery( |
| MediaCastMode::TAB_MIRROR, mirroring_source); |
| UpdateCastModes(); |
| } |
| |
| void MediaRouterUI::OnDefaultPresentationChanged( |
| const PresentationRequest& presentation_request) { |
| presentation_request_.reset(new PresentationRequest(presentation_request)); |
| query_result_manager_->StartSinksQuery( |
| MediaCastMode::DEFAULT, presentation_request_->GetMediaSource()); |
| UpdateCastModes(); |
| } |
| |
| void MediaRouterUI::OnDefaultPresentationRemoved() { |
| presentation_request_.reset(); |
| query_result_manager_->StopSinksQuery(MediaCastMode::DEFAULT); |
| UpdateCastModes(); |
| } |
| |
| void MediaRouterUI::UpdateCastModes() { |
| // Gets updated cast modes from |query_result_manager_| and forwards it to UI. |
| query_result_manager_->GetSupportedCastModes(&cast_modes_); |
| if (ui_initialized_) { |
| handler_->UpdateCastModes( |
| cast_modes_, GetHostFromURL(GetFrameURL())); |
| } |
| } |
| |
| void MediaRouterUI::Close() { |
| ConstrainedWebDialogDelegate* delegate = GetConstrainedDelegate(); |
| if (delegate) { |
| delegate->GetWebDialogDelegate()->OnDialogClosed(std::string()); |
| delegate->OnDialogCloseFromWebUI(); |
| } |
| } |
| |
| void MediaRouterUI::UIInitialized() { |
| ui_initialized_ = true; |
| |
| // Register for Issue updates. |
| if (!issues_observer_) |
| issues_observer_.reset(new UIIssuesObserver(router_, this)); |
| issues_observer_->RegisterObserver(); |
| } |
| |
| bool MediaRouterUI::CreateRoute(const MediaSink::Id& sink_id, |
| MediaCastMode cast_mode) { |
| DCHECK(query_result_manager_.get()); |
| DCHECK(initiator_); |
| |
| // Note that there is a rarely-encountered bug, where the MediaCastMode to |
| // MediaSource mapping could have been updated, between when the user clicked |
| // on the UI to start a create route request, and when this function is |
| // called. However, since the user does not have visibility into the |
| // MediaSource, and that it occurs very rarely in practice, we leave it as-is |
| // for now. |
| MediaSource source = query_result_manager_->GetSourceForCastMode(cast_mode); |
| if (source.Empty()) { |
| LOG(ERROR) << "No corresponding MediaSource for cast mode " << cast_mode; |
| return false; |
| } |
| |
| requesting_route_for_default_source_ = cast_mode == MediaCastMode::DEFAULT; |
| if (requesting_route_for_default_source_ && !presentation_request_) { |
| DLOG(ERROR) << "Requested to create a route for presentation, but " |
| << "presentation request is missing."; |
| return false; |
| } |
| |
| current_route_request_id_ = ++route_request_counter_; |
| GURL origin; |
| if (requesting_route_for_default_source_) { |
| origin = presentation_request_->frame_url().GetOrigin(); |
| } else { |
| // Requesting route for mirroring. Use a placeholder URL as origin. |
| origin = GURL(chrome::kChromeUIMediaRouterURL); |
| } |
| DCHECK(origin.is_valid()); |
| |
| DVLOG(1) << "DoCreateRoute: origin: " << origin; |
| |
| // There are 3 cases. In all cases the MediaRouterUI will need to be notified. |
| // (1) Non-presentation route request (e.g., mirroring). No additional |
| // notification necessary. |
| // (2) Presentation route request for a Presentation API startSession call. |
| // The startSession (CreatePresentationConnectionRequest) will need to be |
| // answered with the |
| // route response. |
| // (3) Browser-initiated presentation route request. If successful, |
| // PresentationServiceDelegateImpl will have to be notified. Note that we |
| // treat subsequent route requests from a Presentation API-initiated dialogs |
| // as browser-initiated. |
| std::vector<MediaRouteResponseCallback> route_response_callbacks; |
| route_response_callbacks.push_back( |
| base::Bind(&MediaRouterUI::OnRouteResponseReceived, |
| weak_factory_.GetWeakPtr(), current_route_request_id_, |
| sink_id)); |
| if (requesting_route_for_default_source_) { |
| if (create_session_request_) { |
| // |create_session_request_| will be nullptr after this call, as the |
| // object will be transferred to the callback. |
| route_response_callbacks.push_back( |
| base::Bind(&CreatePresentationConnectionRequest::HandleRouteResponse, |
| base::Passed(&create_session_request_))); |
| } else if (presentation_service_delegate_) { |
| route_response_callbacks.push_back( |
| base::Bind(&PresentationServiceDelegateImpl::OnRouteResponse, |
| presentation_service_delegate_, *presentation_request_)); |
| } |
| } |
| |
| // Start the timer. |
| route_creation_timer_.Start( |
| FROM_HERE, base::TimeDelta::FromSeconds(kCreateRouteTimeoutSeconds), |
| this, &MediaRouterUI::RouteCreationTimeout); |
| |
| router_->CreateRoute(source.id(), sink_id, origin, |
| initiator_, |
| route_response_callbacks); |
| return true; |
| } |
| |
| void MediaRouterUI::CloseRoute(const MediaRoute::Id& route_id) { |
| router_->CloseRoute(route_id); |
| } |
| |
| void MediaRouterUI::AddIssue(const Issue& issue) { |
| router_->AddIssue(issue); |
| } |
| |
| void MediaRouterUI::ClearIssue(const std::string& issue_id) { |
| router_->ClearIssue(issue_id); |
| } |
| |
| void MediaRouterUI::OnResultsUpdated( |
| const std::vector<MediaSinkWithCastModes>& sinks) { |
| sinks_ = sinks; |
| if (ui_initialized_) |
| handler_->UpdateSinks(sinks_); |
| } |
| |
| void MediaRouterUI::SetIssue(const Issue* issue) { |
| if (ui_initialized_) |
| handler_->UpdateIssue(issue); |
| } |
| |
| void MediaRouterUI::OnRoutesUpdated(const std::vector<MediaRoute>& routes) { |
| routes_ = routes; |
| if (ui_initialized_) |
| handler_->UpdateRoutes(routes_); |
| } |
| |
| void MediaRouterUI::OnRouteResponseReceived(const int route_request_id, |
| const MediaSink::Id& sink_id, |
| const MediaRoute* route, |
| const std::string& presentation_id, |
| const std::string& error) { |
| DVLOG(1) << "OnRouteResponseReceived"; |
| // If we receive a new route that we aren't expecting, do nothing. |
| if (route_request_id != current_route_request_id_) |
| return; |
| |
| if (!route) { |
| // The provider will handle sending an issue for a failed route request. |
| DVLOG(0) << "MediaRouteResponse returned error: " << error; |
| } |
| |
| handler_->OnCreateRouteResponseReceived(sink_id, route); |
| requesting_route_for_default_source_ = false; |
| current_route_request_id_ = -1; |
| route_creation_timer_.Stop(); |
| } |
| |
| void MediaRouterUI::RouteCreationTimeout() { |
| requesting_route_for_default_source_ = false; |
| current_route_request_id_ = -1; |
| |
| base::string16 host = base::UTF8ToUTF16(GetTruncatedHostFromURL( |
| GetFrameURL())); |
| |
| // TODO(apacible): Update error messages based on current cast mode |
| // (e.g. desktop). |
| std::string issue_title = host.empty() ? |
| l10n_util::GetStringUTF8( |
| IDS_MEDIA_ROUTER_ISSUE_CREATE_ROUTE_TIMEOUT_FOR_TAB) : |
| l10n_util::GetStringFUTF8(IDS_MEDIA_ROUTER_ISSUE_CREATE_ROUTE_TIMEOUT, |
| host); |
| |
| Issue issue(issue_title, std::string(), |
| IssueAction(IssueAction::TYPE_DISMISS), |
| std::vector<IssueAction>(), std::string(), Issue::NOTIFICATION, |
| false, std::string()); |
| AddIssue(issue); |
| handler_->NotifyRouteCreationTimeout(); |
| } |
| |
| GURL MediaRouterUI::GetFrameURL() const { |
| return presentation_request_ ? presentation_request_->frame_url() : |
| GURL(); |
| } |
| |
| std::string MediaRouterUI::GetFrameURLHost() const { |
| return GetHostFromURL(GetFrameURL()); |
| } |
| |
| const std::string& MediaRouterUI::GetRouteProviderExtensionId() const { |
| return router_->media_route_provider_extension_id(); |
| } |
| |
| } // namespace media_router |