| // 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 "content/browser/presentation/presentation_service_impl.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/logging.h" |
| #include "base/stl_util.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/presentation_request.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_switches.h" |
| #include "content/public/common/frame_navigate_params.h" |
| |
| using blink::mojom::PresentationConnectionState; |
| using blink::mojom::PresentationError; |
| using blink::mojom::PresentationErrorPtr; |
| using blink::mojom::PresentationErrorType; |
| using blink::mojom::PresentationInfo; |
| using blink::mojom::PresentationInfoPtr; |
| using blink::mojom::ScreenAvailability; |
| |
| namespace content { |
| |
| namespace { |
| |
| static constexpr int kInvalidRequestId = -1; |
| static constexpr size_t kMaxPresentationIdLength = 256; |
| |
| int GetNextRequestId() { |
| static int next_request_id = 0; |
| return ++next_request_id; |
| } |
| |
| void InvokeNewPresentationCallbackWithError( |
| PresentationServiceImpl::NewPresentationCallback callback) { |
| std::move(callback).Run( |
| /** PresentationConnectionResultPtr */ nullptr, |
| PresentationError::New( |
| PresentationErrorType::PREVIOUS_START_IN_PROGRESS, |
| "There is already an unsettled Promise from a previous call " |
| "to start.")); |
| } |
| |
| } // namespace |
| |
| PresentationServiceImpl::PresentationServiceImpl( |
| RenderFrameHost* render_frame_host, |
| WebContents* web_contents, |
| ControllerPresentationServiceDelegate* controller_delegate, |
| ReceiverPresentationServiceDelegate* receiver_delegate) |
| : WebContentsObserver(web_contents), |
| render_frame_host_(render_frame_host), |
| controller_delegate_(controller_delegate), |
| receiver_delegate_(receiver_delegate), |
| start_presentation_request_id_(kInvalidRequestId), |
| binding_(this), |
| // TODO(imcheng): Consider using RenderFrameHost* directly instead of IDs. |
| render_process_id_(render_frame_host->GetProcess()->GetID()), |
| render_frame_id_(render_frame_host->GetRoutingID()), |
| is_main_frame_(!render_frame_host->GetParent()), |
| weak_factory_(this) { |
| DCHECK(render_frame_host_); |
| DCHECK(web_contents); |
| CHECK(render_frame_host_->IsRenderFrameLive()); |
| |
| DVLOG(2) << "PresentationServiceImpl: " << render_process_id_ << ", " |
| << render_frame_id_ << " is main frame: " << is_main_frame_; |
| |
| if (auto* delegate = GetPresentationServiceDelegate()) |
| delegate->AddObserver(render_process_id_, render_frame_id_, this); |
| } |
| |
| PresentationServiceImpl::~PresentationServiceImpl() { |
| DVLOG(2) << __FUNCTION__ << ": " << render_process_id_ << ", " |
| << render_frame_id_; |
| |
| // Call Reset() to inform the PresentationServiceDelegate to clean up. |
| Reset(); |
| |
| if (auto* delegate = GetPresentationServiceDelegate()) |
| delegate->RemoveObserver(render_process_id_, render_frame_id_); |
| } |
| |
| // static |
| std::unique_ptr<PresentationServiceImpl> PresentationServiceImpl::Create( |
| RenderFrameHost* render_frame_host) { |
| DVLOG(2) << __func__ << ": " << render_frame_host->GetProcess()->GetID() |
| << ", " << render_frame_host->GetRoutingID(); |
| WebContents* web_contents = |
| WebContents::FromRenderFrameHost(render_frame_host); |
| DCHECK(web_contents); |
| |
| auto* browser = GetContentClient()->browser(); |
| auto* receiver_delegate = |
| browser->GetReceiverPresentationServiceDelegate(web_contents); |
| |
| // In current implementation, web_contents can be controller or receiver |
| // but not both. |
| auto* controller_delegate = |
| receiver_delegate |
| ? nullptr |
| : browser->GetControllerPresentationServiceDelegate(web_contents); |
| |
| return base::WrapUnique(new PresentationServiceImpl( |
| render_frame_host, web_contents, controller_delegate, receiver_delegate)); |
| } |
| |
| void PresentationServiceImpl::Bind( |
| blink::mojom::PresentationServiceRequest request) { |
| binding_.Bind(std::move(request)); |
| binding_.set_connection_error_handler(base::BindOnce( |
| &PresentationServiceImpl::OnConnectionError, base::Unretained(this))); |
| } |
| |
| void PresentationServiceImpl::SetController( |
| blink::mojom::PresentationControllerPtr controller) { |
| if (controller_) { |
| mojo::ReportBadMessage( |
| "There can only be one PresentationController at any given time."); |
| return; |
| } |
| controller_ = std::move(controller); |
| controller_.set_connection_error_handler(base::BindOnce( |
| &PresentationServiceImpl::OnConnectionError, base::Unretained(this))); |
| } |
| |
| void PresentationServiceImpl::SetReceiver( |
| blink::mojom::PresentationReceiverPtr receiver) { |
| // Presentation receiver virtual web tests (which have the flag set) has no |
| // ReceiverPresentationServiceDelegate implementation. |
| // TODO(imcheng): Refactor content_browser_client to return a no-op |
| // PresentationService instead. |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kForcePresentationReceiverForTesting)) { |
| return; |
| } |
| |
| if (!receiver_delegate_ || !is_main_frame_) { |
| mojo::ReportBadMessage( |
| "SetReceiver can only be called from a " |
| "presentation receiver main frame."); |
| return; |
| } |
| |
| if (receiver_) { |
| mojo::ReportBadMessage("SetReceiver can only be called once."); |
| return; |
| } |
| |
| receiver_ = std::move(receiver); |
| receiver_.set_connection_error_handler(base::BindOnce( |
| &PresentationServiceImpl::OnConnectionError, base::Unretained(this))); |
| receiver_delegate_->RegisterReceiverConnectionAvailableCallback( |
| base::Bind(&PresentationServiceImpl::OnReceiverConnectionAvailable, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void PresentationServiceImpl::ListenForScreenAvailability(const GURL& url) { |
| DVLOG(2) << "ListenForScreenAvailability " << url.spec(); |
| if (!controller_delegate_ || !url.is_valid()) { |
| if (controller_) { |
| controller_->OnScreenAvailabilityUpdated(url, |
| ScreenAvailability::UNAVAILABLE); |
| } |
| return; |
| } |
| |
| if (screen_availability_listeners_.count(url)) |
| return; |
| |
| std::unique_ptr<ScreenAvailabilityListenerImpl> listener( |
| new ScreenAvailabilityListenerImpl(url, this)); |
| if (controller_delegate_->AddScreenAvailabilityListener( |
| render_process_id_, render_frame_id_, listener.get())) { |
| screen_availability_listeners_[url] = std::move(listener); |
| } else { |
| DVLOG(1) << "AddScreenAvailabilityListener failed. Ignoring request."; |
| } |
| } |
| |
| void PresentationServiceImpl::StopListeningForScreenAvailability( |
| const GURL& url) { |
| DVLOG(2) << "StopListeningForScreenAvailability " << url.spec(); |
| if (!controller_delegate_) |
| return; |
| |
| auto listener_it = screen_availability_listeners_.find(url); |
| if (listener_it == screen_availability_listeners_.end()) |
| return; |
| |
| controller_delegate_->RemoveScreenAvailabilityListener( |
| render_process_id_, render_frame_id_, listener_it->second.get()); |
| screen_availability_listeners_.erase(listener_it); |
| } |
| |
| void PresentationServiceImpl::StartPresentation( |
| const std::vector<GURL>& presentation_urls, |
| NewPresentationCallback callback) { |
| DVLOG(2) << "StartPresentation"; |
| |
| // There is a StartPresentation request in progress. To avoid queueing up |
| // requests, the incoming request is rejected. |
| if (start_presentation_request_id_ != kInvalidRequestId) { |
| InvokeNewPresentationCallbackWithError(std::move(callback)); |
| return; |
| } |
| |
| if (!controller_delegate_) { |
| std::move(callback).Run( |
| /** PresentationConnectionResultPtr */ nullptr, |
| PresentationError::New(PresentationErrorType::NO_AVAILABLE_SCREENS, |
| "No screens found.")); |
| return; |
| } |
| |
| start_presentation_request_id_ = GetNextRequestId(); |
| pending_start_presentation_cb_.reset( |
| new NewPresentationCallbackWrapper(std::move(callback))); |
| PresentationRequest request({render_process_id_, render_frame_id_}, |
| presentation_urls, |
| render_frame_host_->GetLastCommittedOrigin()); |
| controller_delegate_->StartPresentation( |
| request, |
| base::BindOnce(&PresentationServiceImpl::OnStartPresentationSucceeded, |
| weak_factory_.GetWeakPtr(), |
| start_presentation_request_id_), |
| base::BindOnce(&PresentationServiceImpl::OnStartPresentationError, |
| weak_factory_.GetWeakPtr(), |
| start_presentation_request_id_)); |
| } |
| |
| void PresentationServiceImpl::ReconnectPresentation( |
| const std::vector<GURL>& presentation_urls, |
| const std::string& presentation_id, |
| NewPresentationCallback callback) { |
| DVLOG(2) << "ReconnectPresentation"; |
| if (!controller_delegate_) { |
| std::move(callback).Run( |
| /** PresentationConnectionResultPtr */ nullptr, |
| PresentationError::New(PresentationErrorType::NO_PRESENTATION_FOUND, |
| "Error joining route: No matching route")); |
| return; |
| } |
| |
| int request_id = RegisterReconnectPresentationCallback(&callback); |
| if (request_id == kInvalidRequestId) { |
| InvokeNewPresentationCallbackWithError(std::move(callback)); |
| return; |
| } |
| |
| PresentationRequest request({render_process_id_, render_frame_id_}, |
| presentation_urls, |
| render_frame_host_->GetLastCommittedOrigin()); |
| controller_delegate_->ReconnectPresentation( |
| request, presentation_id, |
| base::BindOnce(&PresentationServiceImpl::OnReconnectPresentationSucceeded, |
| weak_factory_.GetWeakPtr(), request_id), |
| base::BindOnce(&PresentationServiceImpl::OnReconnectPresentationError, |
| weak_factory_.GetWeakPtr(), request_id)); |
| } |
| |
| int PresentationServiceImpl::RegisterReconnectPresentationCallback( |
| NewPresentationCallback* callback) { |
| if (pending_reconnect_presentation_cbs_.size() >= kMaxQueuedRequests) |
| return kInvalidRequestId; |
| |
| int request_id = GetNextRequestId(); |
| pending_reconnect_presentation_cbs_[request_id] = |
| std::make_unique<NewPresentationCallbackWrapper>(std::move(*callback)); |
| DCHECK_NE(kInvalidRequestId, request_id); |
| return request_id; |
| } |
| |
| void PresentationServiceImpl::ListenForConnectionStateChange( |
| const PresentationInfo& connection) { |
| // NOTE: Blink will automatically transition the connection's state to |
| // 'connected'. |
| if (controller_delegate_) { |
| controller_delegate_->ListenForConnectionStateChange( |
| render_process_id_, render_frame_id_, connection, |
| base::Bind(&PresentationServiceImpl::OnConnectionStateChanged, |
| weak_factory_.GetWeakPtr(), connection)); |
| } |
| } |
| |
| void PresentationServiceImpl::OnStartPresentationSucceeded( |
| int request_id, |
| blink::mojom::PresentationConnectionResultPtr result) { |
| if (request_id != start_presentation_request_id_) |
| return; |
| |
| auto presentation_info = *result->presentation_info; |
| DCHECK(pending_start_presentation_cb_.get()); |
| DCHECK(presentation_info.id.length() <= kMaxPresentationIdLength); |
| pending_start_presentation_cb_->Run(std::move(result), |
| /** PresentationErrorPtr */ nullptr); |
| ListenForConnectionStateChange(presentation_info); |
| pending_start_presentation_cb_.reset(); |
| start_presentation_request_id_ = kInvalidRequestId; |
| } |
| |
| void PresentationServiceImpl::OnStartPresentationError( |
| int request_id, |
| const blink::mojom::PresentationError& error) { |
| if (request_id != start_presentation_request_id_) |
| return; |
| |
| CHECK(pending_start_presentation_cb_.get()); |
| pending_start_presentation_cb_->Run( |
| /** PresentationConnectionResultPtr */ nullptr, |
| PresentationError::New(error)); |
| pending_start_presentation_cb_.reset(); |
| start_presentation_request_id_ = kInvalidRequestId; |
| } |
| |
| void PresentationServiceImpl::OnReconnectPresentationSucceeded( |
| int request_id, |
| blink::mojom::PresentationConnectionResultPtr result) { |
| auto presentation_info = *result->presentation_info; |
| if (RunAndEraseReconnectPresentationMojoCallback( |
| request_id, std::move(result), /** PresentationErrorPtr */ nullptr)) { |
| ListenForConnectionStateChange(presentation_info); |
| } |
| } |
| |
| void PresentationServiceImpl::OnReconnectPresentationError( |
| int request_id, |
| const blink::mojom::PresentationError& error) { |
| RunAndEraseReconnectPresentationMojoCallback( |
| request_id, blink::mojom::PresentationConnectionResultPtr(), |
| PresentationError::New(error)); |
| } |
| |
| bool PresentationServiceImpl::RunAndEraseReconnectPresentationMojoCallback( |
| int request_id, |
| blink::mojom::PresentationConnectionResultPtr result, |
| blink::mojom::PresentationErrorPtr error) { |
| auto it = pending_reconnect_presentation_cbs_.find(request_id); |
| if (it == pending_reconnect_presentation_cbs_.end()) |
| return false; |
| |
| DCHECK(it->second.get()); |
| it->second->Run(std::move(result), std::move(error)); |
| pending_reconnect_presentation_cbs_.erase(it); |
| return true; |
| } |
| |
| void PresentationServiceImpl::SetDefaultPresentationUrls( |
| const std::vector<GURL>& presentation_urls) { |
| DVLOG(2) << "SetDefaultPresentationUrls"; |
| if (!controller_delegate_ || !is_main_frame_) |
| return; |
| |
| if (default_presentation_urls_ == presentation_urls) |
| return; |
| |
| default_presentation_urls_ = presentation_urls; |
| PresentationRequest request({render_process_id_, render_frame_id_}, |
| presentation_urls, |
| render_frame_host_->GetLastCommittedOrigin()); |
| controller_delegate_->SetDefaultPresentationUrls( |
| request, |
| base::Bind(&PresentationServiceImpl::OnDefaultPresentationStarted, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void PresentationServiceImpl::CloseConnection( |
| const GURL& presentation_url, |
| const std::string& presentation_id) { |
| DVLOG(2) << "CloseConnection " << presentation_id; |
| if (controller_delegate_) |
| controller_delegate_->CloseConnection(render_process_id_, render_frame_id_, |
| presentation_id); |
| } |
| |
| void PresentationServiceImpl::Terminate(const GURL& presentation_url, |
| const std::string& presentation_id) { |
| DVLOG(2) << "Terminate " << presentation_id; |
| if (controller_delegate_) |
| controller_delegate_->Terminate(render_process_id_, render_frame_id_, |
| presentation_id); |
| } |
| |
| void PresentationServiceImpl::OnConnectionStateChanged( |
| const PresentationInfo& connection, |
| const PresentationConnectionStateChangeInfo& info) { |
| DVLOG(2) << "PresentationServiceImpl::OnConnectionStateChanged " |
| << "[presentation_id]: " << connection.id |
| << " [state]: " << info.state; |
| if (!controller_) |
| return; |
| |
| if (info.state == PresentationConnectionState::CLOSED) { |
| controller_->OnConnectionClosed(PresentationInfo::New(connection), |
| info.close_reason, info.message); |
| } else { |
| controller_->OnConnectionStateChanged(PresentationInfo::New(connection), |
| info.state); |
| } |
| } |
| |
| bool PresentationServiceImpl::FrameMatches( |
| content::RenderFrameHost* render_frame_host) const { |
| if (!render_frame_host) |
| return false; |
| |
| return render_frame_host->GetProcess()->GetID() == render_process_id_ && |
| render_frame_host->GetRoutingID() == render_frame_id_; |
| } |
| |
| void PresentationServiceImpl::OnConnectionError() { |
| Reset(); |
| } |
| |
| PresentationServiceDelegate* |
| PresentationServiceImpl::GetPresentationServiceDelegate() { |
| return receiver_delegate_ |
| ? static_cast<PresentationServiceDelegate*>(receiver_delegate_) |
| : static_cast<PresentationServiceDelegate*>(controller_delegate_); |
| } |
| |
| // TODO(btolsch): Convert to PresentationConnectionResultPtr. |
| void PresentationServiceImpl::OnReceiverConnectionAvailable( |
| PresentationInfoPtr presentation_info, |
| PresentationConnectionPtr controller_connection_ptr, |
| PresentationConnectionRequest receiver_connection_request) { |
| DVLOG(2) << "PresentationServiceImpl::OnReceiverConnectionAvailable"; |
| |
| receiver_->OnReceiverConnectionAvailable( |
| std::move(presentation_info), std::move(controller_connection_ptr), |
| std::move(receiver_connection_request)); |
| } |
| |
| void PresentationServiceImpl::DidFinishNavigation( |
| NavigationHandle* navigation_handle) { |
| DVLOG(2) << "PresentationServiceImpl::DidNavigateAnyFrame"; |
| if (!navigation_handle->HasCommitted() || |
| !FrameMatches(navigation_handle->GetRenderFrameHost())) { |
| return; |
| } |
| |
| // If a frame navigation is same-document (e.g. navigating to a fragment in |
| // same page) then we do not unregister listeners. |
| DVLOG(2) << "DidNavigateAnyFrame: " |
| << ", is_same_document: " << navigation_handle->IsSameDocument(); |
| if (navigation_handle->IsSameDocument()) |
| return; |
| |
| // Reset if the frame actually navigated. |
| Reset(); |
| } |
| |
| void PresentationServiceImpl::Reset() { |
| DVLOG(2) << "PresentationServiceImpl::Reset"; |
| if (controller_delegate_) |
| controller_delegate_->Reset(render_process_id_, render_frame_id_); |
| |
| if (receiver_delegate_ && is_main_frame_) |
| receiver_delegate_->Reset(render_process_id_, render_frame_id_); |
| |
| default_presentation_urls_.clear(); |
| |
| screen_availability_listeners_.clear(); |
| |
| start_presentation_request_id_ = kInvalidRequestId; |
| pending_start_presentation_cb_.reset(); |
| |
| pending_reconnect_presentation_cbs_.clear(); |
| |
| binding_.Close(); |
| controller_.reset(); |
| receiver_.reset(); |
| } |
| |
| void PresentationServiceImpl::OnDelegateDestroyed() { |
| DVLOG(2) << "PresentationServiceImpl::OnDelegateDestroyed"; |
| controller_delegate_ = nullptr; |
| receiver_delegate_ = nullptr; |
| Reset(); |
| } |
| |
| void PresentationServiceImpl::OnDefaultPresentationStarted( |
| blink::mojom::PresentationConnectionResultPtr result) { |
| auto presentation_info = *result->presentation_info; |
| if (controller_) |
| controller_->OnDefaultPresentationStarted(std::move(result)); |
| |
| // TODO(btolsch): Remove the state-change API in favor of direct |
| // PresentationConnection state use. |
| ListenForConnectionStateChange(presentation_info); |
| } |
| |
| PresentationServiceImpl::ScreenAvailabilityListenerImpl:: |
| ScreenAvailabilityListenerImpl(const GURL& availability_url, |
| PresentationServiceImpl* service) |
| : availability_url_(availability_url), service_(service) { |
| DCHECK(availability_url_.is_valid()); |
| DCHECK(service_); |
| } |
| |
| PresentationServiceImpl::ScreenAvailabilityListenerImpl:: |
| ~ScreenAvailabilityListenerImpl() = default; |
| |
| GURL PresentationServiceImpl::ScreenAvailabilityListenerImpl:: |
| GetAvailabilityUrl() const { |
| return availability_url_; |
| } |
| |
| void PresentationServiceImpl::ScreenAvailabilityListenerImpl:: |
| OnScreenAvailabilityChanged(blink::mojom::ScreenAvailability availability) { |
| if (service_->controller_) { |
| service_->controller_->OnScreenAvailabilityUpdated(availability_url_, |
| availability); |
| } |
| } |
| |
| PresentationServiceImpl::NewPresentationCallbackWrapper:: |
| NewPresentationCallbackWrapper(NewPresentationCallback callback) |
| : callback_(std::move(callback)) {} |
| |
| PresentationServiceImpl::NewPresentationCallbackWrapper:: |
| ~NewPresentationCallbackWrapper() { |
| if (!callback_.is_null()) { |
| std::move(callback_).Run( |
| /** PresentationConnectionResultPtr */ nullptr, |
| PresentationError::New( |
| PresentationErrorType::PRESENTATION_REQUEST_CANCELLED, |
| "The frame is navigating or being destroyed.")); |
| } |
| } |
| |
| void PresentationServiceImpl::NewPresentationCallbackWrapper::Run( |
| blink::mojom::PresentationConnectionResultPtr result, |
| blink::mojom::PresentationErrorPtr error) { |
| DCHECK(!callback_.is_null()); |
| std::move(callback_).Run(std::move(result), std::move(error)); |
| } |
| |
| } // namespace content |