|  | // Copyright 2016 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "content/browser/xr/service/vr_service_impl.h" | 
|  |  | 
|  | #include <algorithm> | 
|  | #include <utility> | 
|  | #include <vector> | 
|  |  | 
|  | #include "base/containers/contains.h" | 
|  | #include "base/dcheck_is_on.h" | 
|  | #include "base/feature_list.h" | 
|  | #include "base/functional/bind.h" | 
|  | #include "base/metrics/histogram_macros.h" | 
|  | #include "base/stl_util.h" | 
|  | #include "base/task/sequenced_task_runner.h" | 
|  | #include "base/time/time.h" | 
|  | #include "base/trace_event/common/trace_event_common.h" | 
|  | #include "build/build_config.h" | 
|  | #include "components/viz/common/surfaces/frame_sink_id.h" | 
|  | #include "content/browser/web_contents/web_contents_impl.h" | 
|  | #include "content/browser/xr/metrics/session_metrics_helper.h" | 
|  | #include "content/browser/xr/service/browser_xr_runtime_impl.h" | 
|  | #include "content/browser/xr/service/xr_permission_results.h" | 
|  | #include "content/browser/xr/service/xr_runtime_manager_impl.h" | 
|  | #include "content/browser/xr/webxr_internals/mojom/webxr_internals.mojom.h" | 
|  | #include "content/browser/xr/webxr_internals/webxr_internals_handler_impl.h" | 
|  | #include "content/public/browser/browser_context.h" | 
|  | #include "content/public/browser/permission_controller.h" | 
|  | #include "content/public/browser/permission_descriptor_util.h" | 
|  | #include "content/public/browser/permission_request_description.h" | 
|  | #include "content/public/browser/render_frame_host.h" | 
|  | #include "content/public/browser/render_process_host.h" | 
|  | #include "content/public/browser/render_widget_host.h" | 
|  | #include "content/public/browser/render_widget_host_view.h" | 
|  | #include "content/public/browser/web_contents.h" | 
|  | #include "content/public/common/origin_util.h" | 
|  | #include "device/vr/buildflags/buildflags.h" | 
|  | #include "device/vr/public/cpp/features.h" | 
|  | #include "device/vr/public/cpp/session_mode.h" | 
|  | #include "device/vr/public/mojom/vr_service.mojom-shared.h" | 
|  | #include "device/vr/public/mojom/xr_device.mojom-shared.h" | 
|  | #include "device/vr/public/mojom/xr_session.mojom-shared.h" | 
|  | #include "third_party/blink/public/common/permissions/permission_utils.h" | 
|  | #include "third_party/blink/public/mojom/permissions/permission_status.mojom-shared.h" | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | device::mojom::XRRuntimeSessionOptionsPtr GetRuntimeOptions( | 
|  | device::mojom::XRSessionOptions* options) { | 
|  | device::mojom::XRRuntimeSessionOptionsPtr runtime_options = | 
|  | device::mojom::XRRuntimeSessionOptions::New(); | 
|  | runtime_options->mode = options->mode; | 
|  | return runtime_options; | 
|  | } | 
|  |  | 
|  | // Helper, returns collection of permissions required for XR session creation | 
|  | // for session with mode set to |mode|. The order in the result does not matter | 
|  | // as the permissions API does not honor it. | 
|  | std::vector<blink::PermissionType> GetRequiredPermissionsForMode( | 
|  | device::mojom::XRSessionMode mode) { | 
|  | std::vector<blink::PermissionType> permissions; | 
|  |  | 
|  | auto mode_permission = content::XrPermissionResults::GetPermissionFor(mode); | 
|  | if (mode_permission) { | 
|  | permissions.push_back(*mode_permission); | 
|  | } | 
|  |  | 
|  | return permissions; | 
|  | } | 
|  |  | 
|  | // Helper, returns collection of permissions required for XR session creation | 
|  | // for session with enabled features listed in |required_features| and | 
|  | // |optional_features|. The order in the result does not matter as the | 
|  | // permissions API does not honor it. | 
|  | std::vector<blink::PermissionType> GetRequiredPermissionsForFeatures( | 
|  | const std::unordered_set<device::mojom::XRSessionFeature>& | 
|  | required_features, | 
|  | const std::unordered_set<device::mojom::XRSessionFeature>& | 
|  | optional_features) { | 
|  | std::vector<blink::PermissionType> permissions; | 
|  |  | 
|  | for (const auto& required_feature : required_features) { | 
|  | auto feature_permission = | 
|  | content::XrPermissionResults::GetPermissionFor(required_feature); | 
|  | if (feature_permission && | 
|  | !base::Contains(permissions, *feature_permission)) { | 
|  | permissions.push_back(*feature_permission); | 
|  | } | 
|  | } | 
|  |  | 
|  | for (const auto& optional_feature : optional_features) { | 
|  | auto feature_permission = | 
|  | content::XrPermissionResults::GetPermissionFor(optional_feature); | 
|  | if (feature_permission && | 
|  | !base::Contains(permissions, *feature_permission)) { | 
|  | permissions.push_back(*feature_permission); | 
|  | } | 
|  | } | 
|  |  | 
|  | return permissions; | 
|  | } | 
|  |  | 
|  | // TODO(crbug.com/40930146): Replace with std::ranges::set_difference | 
|  | std::unordered_set<device::mojom::XRSessionFeature> GetMissingRequiredFeatures( | 
|  | const std::unordered_set<device::mojom::XRSessionFeature>& enabled_features, | 
|  | const std::unordered_set<device::mojom::XRSessionFeature>& | 
|  | required_features) { | 
|  | DVLOG(3) << __func__ | 
|  | << ": enabled_features.size()=" << enabled_features.size(); | 
|  |  | 
|  | std::unordered_set<device::mojom::XRSessionFeature> missing_required_features; | 
|  |  | 
|  | for (const auto& required_feature : required_features) { | 
|  | if (!base::Contains(enabled_features, required_feature)) { | 
|  | DVLOG(2) << __func__ | 
|  | << ": one of the required features was not enabled on the " | 
|  | "created session, feature: " | 
|  | << required_feature; | 
|  | missing_required_features.insert(required_feature); | 
|  | } | 
|  | } | 
|  |  | 
|  | return missing_required_features; | 
|  | } | 
|  |  | 
|  | void RejectSession(device::mojom::VRService::RequestSessionCallback callback, | 
|  | size_t trace_id, | 
|  | device::mojom::RequestSessionError error, | 
|  | const std::string& failure_reason_description, | 
|  | std::unordered_set<device::mojom::XRSessionFeature>* | 
|  | rejected_features = nullptr) { | 
|  | DVLOG(2) << __func__ | 
|  | << ": failure reason description=" << failure_reason_description; | 
|  |  | 
|  | webxr::mojom::SessionRejectedRecordPtr session_rejected_record = | 
|  | webxr::mojom::SessionRejectedRecord::New(); | 
|  | session_rejected_record->trace_id = trace_id; | 
|  | session_rejected_record->failure_reason = error; | 
|  | session_rejected_record->rejected_time = base::Time::Now(); | 
|  | session_rejected_record->failure_reason_description = | 
|  | failure_reason_description; | 
|  | if (rejected_features) { | 
|  | session_rejected_record->rejected_features.assign( | 
|  | rejected_features->begin(), rejected_features->end()); | 
|  | } | 
|  |  | 
|  | auto* runtime_manager_impl = static_cast<content::XRRuntimeManagerImpl*>( | 
|  | content::XRRuntimeManager::GetInstanceIfCreated()); | 
|  | if (runtime_manager_impl) { | 
|  | runtime_manager_impl->GetLoggerManager().RecordSessionRejected( | 
|  | std::move(session_rejected_record)); | 
|  | } | 
|  |  | 
|  | std::move(callback).Run( | 
|  | device::mojom::RequestSessionResult::NewFailureReason(error)); | 
|  | } | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | namespace content { | 
|  |  | 
|  | VRServiceImpl::SessionRequestData::SessionRequestData( | 
|  | device::mojom::XRSessionOptionsPtr options, | 
|  | device::mojom::VRService::RequestSessionCallback callback, | 
|  | device::mojom::XRDeviceId runtime_id) | 
|  | : callback(std::move(callback)), | 
|  | required_features(options->required_features.begin(), | 
|  | options->required_features.end()), | 
|  | optional_features(options->optional_features.begin(), | 
|  | options->optional_features.end()), | 
|  | options(std::move(options)), | 
|  | runtime_id(runtime_id) {} | 
|  |  | 
|  | VRServiceImpl::SessionRequestData::~SessionRequestData() { | 
|  | // In some cases, we may get dropped before the VRService pipe is closed. In | 
|  | // these cases we need to try to ensure that the callback is run or else we | 
|  | // hit DCHECKs for dropping the callback without closing the pipe. | 
|  | // This most often occurs when the Permissions prompt is dismissed. | 
|  | if (callback) { | 
|  | RejectSession(std::move(callback), options->trace_id, | 
|  | device::mojom::RequestSessionError::UNKNOWN_FAILURE, | 
|  | "SessionRequestData destroyed without running callback."); | 
|  | } | 
|  | } | 
|  |  | 
|  | VRServiceImpl::SessionRequestData::SessionRequestData(SessionRequestData&&) = | 
|  | default; | 
|  |  | 
|  | VRServiceImpl::XrCompatibleCallback::XrCompatibleCallback( | 
|  | device::mojom::VRService::MakeXrCompatibleCallback callback) | 
|  | : callback(std::move(callback)) {} | 
|  |  | 
|  | VRServiceImpl::XrCompatibleCallback::XrCompatibleCallback( | 
|  | XrCompatibleCallback&& wrapper) { | 
|  | this->callback = std::move(wrapper.callback); | 
|  | } | 
|  |  | 
|  | VRServiceImpl::XrCompatibleCallback::~XrCompatibleCallback() { | 
|  | if (!callback.is_null()) | 
|  | std::move(callback).Run( | 
|  | device::mojom::XrCompatibleResult::kNoDeviceAvailable); | 
|  | } | 
|  |  | 
|  | VRServiceImpl::VRServiceImpl(content::RenderFrameHost* render_frame_host) | 
|  | : WebContentsObserver( | 
|  | content::WebContents::FromRenderFrameHost(render_frame_host)), | 
|  | render_frame_host_(render_frame_host), | 
|  | in_focused_frame_(render_frame_host->GetView()->HasFocus()) { | 
|  | DCHECK(render_frame_host_); | 
|  | DVLOG(2) << __func__; | 
|  |  | 
|  | runtime_manager_ = | 
|  | XRRuntimeManagerImpl::GetOrCreateInstance(*GetWebContents()); | 
|  | runtime_manager_->AddService(this); | 
|  |  | 
|  | magic_window_controllers_.set_disconnect_handler(base::BindRepeating( | 
|  | &VRServiceImpl::OnInlineSessionDisconnected, | 
|  | base::Unretained(this)));  // Unretained is OK since the collection is | 
|  | // owned by VRServiceImpl. | 
|  | } | 
|  |  | 
|  | // Constructor for testing. | 
|  | VRServiceImpl::VRServiceImpl(base::PassKey<XRRuntimeManagerTest>) | 
|  | : render_frame_host_(nullptr) { | 
|  | DVLOG(2) << __func__; | 
|  | runtime_manager_ = XRRuntimeManagerImpl::GetOrCreateInstanceForTesting(); | 
|  | runtime_manager_->AddService(this); | 
|  | } | 
|  |  | 
|  | VRServiceImpl::~VRServiceImpl() { | 
|  | DVLOG(2) << __func__; | 
|  | // Ensure that any active magic window sessions are disconnected to avoid | 
|  | // collisions when a new session starts. See https://crbug.com/40655152, the | 
|  | // disconnect handler doesn't get called automatically on page navigation. | 
|  | for (auto it = magic_window_controllers_.begin(); | 
|  | it != magic_window_controllers_.end(); ++it) { | 
|  | OnInlineSessionDisconnected(it.id()); | 
|  | } | 
|  | magic_window_controllers_.Clear(); | 
|  |  | 
|  | OnExitPresent(); | 
|  |  | 
|  | runtime_manager_->RemoveService(this); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::Create( | 
|  | content::RenderFrameHost* render_frame_host, | 
|  | mojo::PendingReceiver<device::mojom::VRService> receiver) { | 
|  | DVLOG(2) << __func__; | 
|  | std::unique_ptr<VRServiceImpl> vr_service_impl = | 
|  | std::make_unique<VRServiceImpl>(render_frame_host); | 
|  |  | 
|  | VRServiceImpl* impl = vr_service_impl.get(); | 
|  | impl->receiver_ = mojo::MakeSelfOwnedReceiver(std::move(vr_service_impl), | 
|  | std::move(receiver)); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::InitializationComplete() { | 
|  | // After initialization has completed, we can correctly answer | 
|  | // supportsSession, and can provide correct display capabilities. | 
|  | DVLOG(2) << __func__; | 
|  | initialization_complete_ = true; | 
|  |  | 
|  | ResolvePendingRequests(); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::SetClient( | 
|  | mojo::PendingRemote<device::mojom::VRServiceClient> service_client) { | 
|  | if (service_client_) { | 
|  | mojo::ReportBadMessage("ServiceClient should only be set once."); | 
|  | return; | 
|  | } | 
|  |  | 
|  | DVLOG(2) << __func__; | 
|  | service_client_.Bind(std::move(service_client)); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::ResolvePendingRequests() { | 
|  | DVLOG(2) << __func__ | 
|  | << ": pending_requests_.size()=" << pending_requests_.size(); | 
|  | for (auto& callback : pending_requests_) { | 
|  | std::move(callback).Run(); | 
|  | } | 
|  | pending_requests_.clear(); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::RuntimesChanged() { | 
|  | DVLOG(2) << __func__; | 
|  | if (service_client_) { | 
|  | service_client_->OnDeviceChanged(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::OnWebContentsFocused(content::RenderWidgetHost* host) { | 
|  | OnWebContentsFocusChanged(host, true); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::OnWebContentsLostFocus(content::RenderWidgetHost* host) { | 
|  | OnWebContentsFocusChanged(host, false); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::RenderFrameDeleted(content::RenderFrameHost* host) { | 
|  | DVLOG(2) << __func__; | 
|  | if (host != render_frame_host_) | 
|  | return; | 
|  |  | 
|  | // Clear out the render_frame_host_ before doing any closing activities. | 
|  | render_frame_host_ = nullptr; | 
|  |  | 
|  | // Receiver should always be live here, as this is a SelfOwnedReceiver. | 
|  | // Close the receiver (and delete this VrServiceImpl) when the RenderFrameHost | 
|  | // is deleted. | 
|  | DCHECK(receiver_.get()); | 
|  | receiver_->Close(); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::OnWebContentsFocusChanged(content::RenderWidgetHost* host, | 
|  | bool focused) { | 
|  | if (!render_frame_host_->GetView() || | 
|  | render_frame_host_->GetView()->GetRenderWidgetHost() != host) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | in_focused_frame_ = focused; | 
|  |  | 
|  | for (const auto& controller : magic_window_controllers_) | 
|  | controller->SetFrameDataRestricted(!focused); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::OnInlineSessionCreated( | 
|  | SessionRequestData request, | 
|  | device::mojom::XRRuntimeSessionResultPtr session_result) { | 
|  | if (!session_result) { | 
|  | TRACE_EVENT("xr", | 
|  | "VRServiceImpl::OnInlineSessionCreated: no session_result", | 
|  | perfetto::Flow::Global(request.options->trace_id)); | 
|  |  | 
|  | RejectSession(std::move(request.callback), request.options->trace_id, | 
|  | device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR, | 
|  | "Runtime did not provide a session."); | 
|  | return; | 
|  | } | 
|  |  | 
|  | mojo::Remote<device::mojom::XRSessionController> controller( | 
|  | std::move(session_result->controller)); | 
|  | // Start giving out magic window data if we are focused. | 
|  | controller->SetFrameDataRestricted(!in_focused_frame_); | 
|  |  | 
|  | auto id = magic_window_controllers_.Add(std::move(controller)); | 
|  | DVLOG(2) << __func__ << ": session_id=" << id.GetUnsafeValue() | 
|  | << " runtime_id=" << request.runtime_id; | 
|  |  | 
|  | auto* session = session_result->session.get(); | 
|  | std::unordered_set<device::mojom::XRSessionFeature> enabled_features( | 
|  | session->enabled_features.begin(), session->enabled_features.end()); | 
|  |  | 
|  | auto missing_required_features = | 
|  | GetMissingRequiredFeatures(enabled_features, request.required_features); | 
|  | if (!missing_required_features.empty()) { | 
|  | // UNKNOWN_FAILURE since a runtime should not return a session if there | 
|  | // exists a required feature that was not enabled - this would signify a bug | 
|  | // in the runtime. | 
|  |  | 
|  | TRACE_EVENT( | 
|  | "xr", | 
|  | "VRServiceImpl::OnInlineSessionCreated: required feature not granted", | 
|  | perfetto::Flow::Global(request.options->trace_id)); | 
|  |  | 
|  | RejectSession(std::move(request.callback), request.options->trace_id, | 
|  | device::mojom::RequestSessionError::UNKNOWN_FAILURE, | 
|  | "Required feature not granted.", &missing_required_features); | 
|  | return; | 
|  | } | 
|  |  | 
|  | mojo::PendingRemote<device::mojom::XRSessionMetricsRecorder> | 
|  | session_metrics_recorder = GetSessionMetricsHelper()->StartInlineSession( | 
|  | *(request.options), enabled_features, id.GetUnsafeValue()); | 
|  |  | 
|  | OnSessionCreated( | 
|  | std::move(request), std::move(session_result->session), | 
|  | std::move(session_metrics_recorder), | 
|  | mojo::PendingRemote<device::mojom::WebXrInternalsRendererListener>()); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::OnImmersiveSessionCreated( | 
|  | SessionRequestData request, | 
|  | device::mojom::XRRuntimeSessionResultPtr session_result) { | 
|  | DCHECK(request.options); | 
|  | if (!session_result) { | 
|  | TRACE_EVENT("xr", | 
|  | "VRServiceImpl::OnImmersiveSessionCreated: no session_result", | 
|  | perfetto::Flow::Global(request.options->trace_id)); | 
|  |  | 
|  | RejectSession(std::move(request.callback), request.options->trace_id, | 
|  | device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR, | 
|  | "Runtime did not provide a session."); | 
|  | return; | 
|  | } | 
|  |  | 
|  | auto* session = session_result->session.get(); | 
|  | std::unordered_set<device::mojom::XRSessionFeature> enabled_features( | 
|  | session->enabled_features.begin(), session->enabled_features.end()); | 
|  |  | 
|  | auto missing_required_features = | 
|  | GetMissingRequiredFeatures(enabled_features, request.required_features); | 
|  | if (!missing_required_features.empty()) { | 
|  | // UNKNOWN_FAILURE since a runtime should not return a session if there | 
|  | // exists a required feature that was not enabled - this would signify a bug | 
|  | // in the runtime. | 
|  |  | 
|  | TRACE_EVENT("xr", | 
|  | "VRServiceImpl::OnImmersiveSessionCreated: required feature " | 
|  | "not granted", | 
|  | perfetto::Flow::Global(request.options->trace_id)); | 
|  |  | 
|  | RejectSession(std::move(request.callback), request.options->trace_id, | 
|  | device::mojom::RequestSessionError::UNKNOWN_FAILURE, | 
|  | "Required feature not granted.", &missing_required_features); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Get the metrics tracker for the new immersive session | 
|  | mojo::PendingRemote<device::mojom::XRSessionMetricsRecorder> | 
|  | session_metrics_recorder = | 
|  | GetSessionMetricsHelper()->StartImmersiveSession( | 
|  | request.runtime_id, *(request.options), enabled_features); | 
|  |  | 
|  | render_frame_host_->GetProcess()->OnImmersiveXrSessionStarted(); | 
|  | has_immersive_session_ = true; | 
|  |  | 
|  | // If the session specified a FrameSinkId that means that it is handling its | 
|  | // own compositing in a way that we should notify the WebContents about. | 
|  | if (session_result->frame_sink_id) { | 
|  | if (session_result->frame_sink_id->is_valid()) { | 
|  | static_cast<WebContentsImpl*>(GetWebContents()) | 
|  | ->OnXrHasRenderTarget(*session_result->frame_sink_id); | 
|  | } else { | 
|  | DLOG(ERROR) << __func__ << " frame_sink_id was specified but was invalid"; | 
|  | } | 
|  | } | 
|  |  | 
|  | OnSessionCreated(std::move(request), std::move(session_result->session), | 
|  | std::move(session_metrics_recorder), | 
|  | runtime_manager_->GetLoggerManager().BindRenderListener()); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::OnInlineSessionDisconnected( | 
|  | mojo::RemoteSetElementId session_id) { | 
|  | DVLOG(2) << __func__ << ": session_id=" << session_id.GetUnsafeValue(); | 
|  | // Notify metrics helper that inline session was stopped. | 
|  | auto* metrics_helper = GetSessionMetricsHelper(); | 
|  | metrics_helper->StopAndRecordInlineSession(session_id.GetUnsafeValue()); | 
|  | } | 
|  |  | 
|  | SessionMetricsHelper* VRServiceImpl::GetSessionMetricsHelper() { | 
|  | content::WebContents* web_contents = | 
|  | content::WebContents::FromRenderFrameHost(render_frame_host_); | 
|  | SessionMetricsHelper* metrics_helper = | 
|  | SessionMetricsHelper::FromWebContents(web_contents); | 
|  | if (!metrics_helper) { | 
|  | // This will only happen if we are not already in VR; set start params | 
|  | // accordingly. | 
|  | metrics_helper = SessionMetricsHelper::CreateForWebContents(web_contents); | 
|  | } | 
|  |  | 
|  | return metrics_helper; | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::OnSessionCreated( | 
|  | SessionRequestData request, | 
|  | device::mojom::XRSessionPtr session, | 
|  | mojo::PendingRemote<device::mojom::XRSessionMetricsRecorder> | 
|  | session_metrics_recorder, | 
|  | mojo::PendingRemote<device::mojom::WebXrInternalsRendererListener> | 
|  | xr_internals_listener) { | 
|  | DVLOG(2) << __func__ << ": session_runtime_id=" << request.runtime_id; | 
|  |  | 
|  | // Not checking for validity of |session|, since that's done by | 
|  | // |OnInlineSessionCreated| and |OnImmersiveSessionCreated|. | 
|  |  | 
|  | UMA_HISTOGRAM_ENUMERATION("XR.RuntimeUsed", request.runtime_id); | 
|  |  | 
|  | TRACE_EVENT("xr", "VRServiceImpl::OnSessionCreated: succeeded", | 
|  | perfetto::Flow::Global(request.options->trace_id)); | 
|  |  | 
|  | mojo::Remote<device::mojom::XRSessionClient> client; | 
|  | session->client_receiver = client.BindNewPipeAndPassReceiver(); | 
|  |  | 
|  | client->OnVisibilityStateChanged(visibility_state_); | 
|  | session_clients_.Add(std::move(client)); | 
|  |  | 
|  | auto success = device::mojom::RequestSessionSuccess::New(); | 
|  | success->session = std::move(session); | 
|  | success->metrics_recorder = std::move(session_metrics_recorder); | 
|  | success->trace_id = request.options->trace_id; | 
|  | success->xr_internals_listener = std::move(xr_internals_listener); | 
|  |  | 
|  | std::move(request.callback) | 
|  | .Run(device::mojom::RequestSessionResult::NewSuccess(std::move(success))); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::RequestSession( | 
|  | device::mojom::XRSessionOptionsPtr options, | 
|  | device::mojom::VRService::RequestSessionCallback callback) { | 
|  | DVLOG(2) << __func__; | 
|  | DCHECK(options); | 
|  |  | 
|  | webxr::mojom::SessionRequestedRecordPtr session_requested_record = | 
|  | webxr::mojom::SessionRequestedRecord::New(); | 
|  | session_requested_record->options = options->Clone(); | 
|  | session_requested_record->requested_time = base::Time::Now(); | 
|  | runtime_manager_->GetLoggerManager().RecordSessionRequested( | 
|  | std::move(session_requested_record)); | 
|  |  | 
|  | // Queue the request to get to when initialization has completed. | 
|  | if (!initialization_complete_) { | 
|  | DVLOG(2) << __func__ << ": initialization not yet complete, defer request"; | 
|  | pending_requests_.push_back( | 
|  | base::BindOnce(&VRServiceImpl::RequestSession, base::Unretained(this), | 
|  | std::move(options), std::move(callback))); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (runtime_manager_->IsOtherClientPresenting(this) || | 
|  | runtime_manager_->HasPendingImmersiveRequest()) { | 
|  | DVLOG(2) << __func__ | 
|  | << ": can't create sessions while an immersive session exists"; | 
|  |  | 
|  | // Can't create sessions while an immersive session exists. | 
|  | RejectSession( | 
|  | std::move(callback), options->trace_id, | 
|  | device::mojom::RequestSessionError::EXISTING_IMMERSIVE_SESSION, | 
|  | "There is an existing immersive session."); | 
|  | return; | 
|  | } | 
|  |  | 
|  | auto* runtime = runtime_manager_->GetRuntimeForOptions(options.get()); | 
|  | if (!runtime) { | 
|  | RejectSession(std::move(callback), options->trace_id, | 
|  | device::mojom::RequestSessionError::NO_RUNTIME_FOUND, | 
|  | "No runtime found for the given session options."); | 
|  | return; | 
|  | } | 
|  |  | 
|  | const bool has_user_activation = | 
|  | render_frame_host_->HasTransientUserActivation(); | 
|  | if (!has_user_activation) { | 
|  | // User activation is verified blink-side, so this should never fail | 
|  | // (everything that happens up to this point should not take enough time for | 
|  | // the user activation to expire). Treat lack of user activation as unknown | 
|  | // failure: | 
|  | RejectSession(std::move(callback), options->trace_id, | 
|  | device::mojom::RequestSessionError::UNKNOWN_FAILURE, | 
|  | "Missing user activation."); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // The consent flow cannot differentiate between optional and required | 
|  | // features, but we don't need to block creation if an optional feature is | 
|  | // not supported. Remove all unsupported optional features from the | 
|  | // optional_features collection before handing it off. | 
|  | std::erase_if(options->optional_features, [runtime](auto& feature) { | 
|  | return !runtime->SupportsFeature(feature); | 
|  | }); | 
|  |  | 
|  | SessionRequestData request(std::move(options), std::move(callback), | 
|  | runtime->GetId()); | 
|  |  | 
|  | GetPermissionStatus(std::move(request), runtime); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::DoRequestPermissions( | 
|  | const std::vector<blink::PermissionType> request_permissions, | 
|  | base::OnceCallback<void(const std::vector<PermissionResult>&)> | 
|  | result_callback) { | 
|  | PermissionController* permission_controller = | 
|  | GetWebContents()->GetBrowserContext()->GetPermissionController(); | 
|  | CHECK(permission_controller); | 
|  |  | 
|  | permission_controller->RequestPermissionsFromCurrentDocument( | 
|  | render_frame_host_, | 
|  | PermissionRequestDescription( | 
|  | PermissionDescriptorUtil:: | 
|  | CreatePermissionDescriptorForPermissionTypes(request_permissions), | 
|  | /*user_gesture=*/true), | 
|  | std::move(result_callback)); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::GetPermissionStatus(SessionRequestData request, | 
|  | BrowserXRRuntimeImpl* runtime) { | 
|  | DVLOG(2) << __func__; | 
|  | DCHECK(request.options); | 
|  | DCHECK(runtime); | 
|  | DCHECK_EQ(runtime->GetId(), request.runtime_id); | 
|  |  | 
|  | #if BUILDFLAG(ENABLE_OPENXR) | 
|  | if (request.options->mode == device::mojom::XRSessionMode::kImmersiveAr && | 
|  | runtime->GetId() == device::mojom::XRDeviceId::OPENXR_DEVICE_ID) { | 
|  | DCHECK(device::features::IsOpenXrArEnabled()); | 
|  | } | 
|  | #endif | 
|  |  | 
|  | // Need to calculate the permissions before the call below, as otherwise | 
|  | // std::move nulls options out before `GetRequiredPermissions()` runs. | 
|  | const std::vector<blink::PermissionType> permissions_for_mode = | 
|  | GetRequiredPermissionsForMode(request.options->mode); | 
|  |  | 
|  | DoRequestPermissions( | 
|  | permissions_for_mode, | 
|  | base::BindOnce(&VRServiceImpl::OnPermissionResultsForMode, | 
|  | weak_ptr_factory_.GetWeakPtr(), std::move(request), | 
|  | permissions_for_mode)); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::OnPermissionResultsForMode( | 
|  | SessionRequestData request, | 
|  | const std::vector<blink::PermissionType>& permissions, | 
|  | const std::vector<PermissionResult>& results) { | 
|  | DVLOG(2) << __func__ << ": permissions.size()=" << permissions.size(); | 
|  | DCHECK_EQ(permissions.size(), results.size()); | 
|  |  | 
|  | // Prolong the user activation since the user may have taken long enough to | 
|  | // answer the permission prompts that the transient user activation expired. | 
|  | // This is fine to do here, since we enforce that the activation existed prior | 
|  | // to requesting permissions. | 
|  | DVLOG(3) << __func__ << ": prolonging user activation, current status=" | 
|  | << render_frame_host_->HasTransientUserActivation(); | 
|  | render_frame_host_->NotifyUserActivation( | 
|  | blink::mojom::UserActivationNotificationType::kInteraction); | 
|  |  | 
|  | const XrPermissionResults permission_results(permissions, results); | 
|  |  | 
|  | bool is_consent_granted = | 
|  | permission_results.HasPermissionsFor(request.options->mode); | 
|  | DVLOG(2) << __func__ << ": is_consent_granted=" << is_consent_granted; | 
|  |  | 
|  | if (!is_consent_granted) { | 
|  | RejectSession(std::move(request.callback), request.options->trace_id, | 
|  | device::mojom::RequestSessionError::USER_DENIED_CONSENT, | 
|  | "Consent was not granted for the requested mode."); | 
|  | return; | 
|  | } | 
|  |  | 
|  | const std::vector<blink::PermissionType> permissions_for_features = | 
|  | GetRequiredPermissionsForFeatures(request.required_features, | 
|  | request.optional_features); | 
|  |  | 
|  | auto result_callback = | 
|  | base::BindOnce(&VRServiceImpl::OnPermissionResultsForFeatures, | 
|  | weak_ptr_factory_.GetWeakPtr(), std::move(request), | 
|  | permissions_for_features); | 
|  | if (permissions_for_features.empty()) { | 
|  | std::move(result_callback).Run({}); | 
|  | return; | 
|  | } | 
|  |  | 
|  | DoRequestPermissions(permissions_for_features, std::move(result_callback)); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::OnPermissionResultsForFeatures( | 
|  | SessionRequestData request, | 
|  | const std::vector<blink::PermissionType>& permissions, | 
|  | const std::vector<PermissionResult>& results) { | 
|  | const XrPermissionResults permission_results(permissions, results); | 
|  |  | 
|  | std::unordered_set<device::mojom::XRSessionFeature> rejected_features; | 
|  | for (auto& required_feature : request.required_features) { | 
|  | if (!permission_results.HasPermissionsFor(required_feature)) { | 
|  | DVLOG(1) << __func__ << ": required_feature=" << required_feature | 
|  | << " lacks neccessary permissions"; | 
|  |  | 
|  | rejected_features.insert(required_feature); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (!rejected_features.empty()) { | 
|  | RejectSession(std::move(request.callback), request.options->trace_id, | 
|  | device::mojom::RequestSessionError::USER_DENIED_CONSENT, | 
|  | "Lacks necessary permissions for the required feature.", | 
|  | &rejected_features); | 
|  | return; | 
|  | } | 
|  |  | 
|  | std::unordered_set<device::mojom::XRSessionFeature> granted_optional_features; | 
|  |  | 
|  | for (auto& optional_feature : request.optional_features) { | 
|  | if (permission_results.HasPermissionsFor(optional_feature)) { | 
|  | granted_optional_features.insert(optional_feature); | 
|  | } else { | 
|  | DVLOG(2) << __func__ << ": optional_feature=" << optional_feature | 
|  | << " lacks neccessary permissions"; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Replace optional features on the request with the ones that have been | 
|  | // granted by the user: | 
|  | std::swap(request.optional_features, granted_optional_features); | 
|  |  | 
|  | // Re-check for another client instance after a potential user consent. | 
|  | if (runtime_manager_->IsOtherClientPresenting(this)) { | 
|  | // Can't create sessions while an immersive session exists. | 
|  | RejectSession( | 
|  | std::move(request.callback), request.options->trace_id, | 
|  | device::mojom::RequestSessionError::EXISTING_IMMERSIVE_SESSION, | 
|  | "Another client started presenting while waiting for permissions."); | 
|  | return; | 
|  | } | 
|  |  | 
|  | EnsureRuntimeInstalled(std::move(request), nullptr); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::EnsureRuntimeInstalled(SessionRequestData request, | 
|  | BrowserXRRuntimeImpl* runtime) { | 
|  | DVLOG(2) << __func__; | 
|  |  | 
|  | // If we were not provided the runtime, try to get it again. | 
|  | if (!runtime) | 
|  | runtime = runtime_manager_->GetRuntimeForOptions(request.options.get()); | 
|  |  | 
|  | // Ensure that it's the same runtime as the one we expect. | 
|  | if (!runtime || runtime->GetId() != request.runtime_id) { | 
|  | DVLOG(1) << __func__ | 
|  | << ": failed to obtain the runtime or the runtime id does not " | 
|  | "match the expected ID, request.runtime_id=" | 
|  | << request.runtime_id; | 
|  |  | 
|  | RejectSession(std::move(request.callback), request.options->trace_id, | 
|  | device::mojom::RequestSessionError::RUNTIMES_CHANGED, | 
|  | "failed to obtain the runtime or the runtime id does not " | 
|  | "match the expected ID."); | 
|  | return; | 
|  | } | 
|  |  | 
|  | runtime->EnsureInstalled( | 
|  | render_frame_host_->GetProcess()->GetDeprecatedID(), | 
|  | render_frame_host_->GetRoutingID(), | 
|  | base::BindOnce(&VRServiceImpl::OnInstallResult, | 
|  | weak_ptr_factory_.GetWeakPtr(), std::move(request))); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::OnInstallResult(SessionRequestData request, | 
|  | bool install_succeeded) { | 
|  | DVLOG(2) << __func__ << ": install_succeeded=" << install_succeeded; | 
|  |  | 
|  | if (!install_succeeded) { | 
|  | RejectSession(std::move(request.callback), request.options->trace_id, | 
|  | device::mojom::RequestSessionError::RUNTIME_INSTALL_FAILURE, | 
|  | "Runtime installation failed."); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Prolong the user activation since the user may have taken long enough to | 
|  | // install the runtime that the transient user activation expired. This is | 
|  | // fine to do here, since we enforce that the activation existed prior to | 
|  | // kicking off installation. | 
|  | DVLOG(3) << __func__ << ": prolonging user activation, current status=" | 
|  | << render_frame_host_->HasTransientUserActivation(); | 
|  | render_frame_host_->NotifyUserActivation( | 
|  | blink::mojom::UserActivationNotificationType::kInteraction); | 
|  |  | 
|  | DoRequestSession(std::move(request)); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::DoRequestSession(SessionRequestData request) { | 
|  | DVLOG(2) << __func__; | 
|  | // Get the runtime again, since we're running in an async context | 
|  | // and the pointer returned from `GetRuntimeForOptions` is non-owning. | 
|  | auto* runtime = runtime_manager_->GetRuntimeForOptions(request.options.get()); | 
|  |  | 
|  | // Ensure that it's the same runtime as the one we expect. | 
|  | if (!runtime || runtime->GetId() != request.runtime_id) { | 
|  | TRACE_EVENT("xr", "VRServiceImpl::DoRequestSession: mismatching runtime", | 
|  | perfetto::Flow::Global(request.options->trace_id)); | 
|  |  | 
|  | RejectSession(std::move(request.callback), request.options->trace_id, | 
|  | device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR, | 
|  | "Mismatching runtime or invalid runtime."); | 
|  | return; | 
|  | } | 
|  |  | 
|  | TRACE_EVENT_INSTANT1("xr", "GetRuntimeForOptions", TRACE_EVENT_SCOPE_THREAD, | 
|  | "id", request.runtime_id); | 
|  |  | 
|  | auto runtime_options = GetRuntimeOptions(request.options.get()); | 
|  | // Make the resolved enabled features available to the runtime. | 
|  |  | 
|  | runtime_options->required_features.assign(request.required_features.begin(), | 
|  | request.required_features.end()); | 
|  | runtime_options->optional_features.assign(request.optional_features.begin(), | 
|  | request.optional_features.end()); | 
|  |  | 
|  | if constexpr (BUILDFLAG(IS_ANDROID)) { | 
|  | bool send_renderer_information = false; | 
|  | #if BUILDFLAG(ENABLE_ARCORE) | 
|  | send_renderer_information = | 
|  | send_renderer_information || | 
|  | request.runtime_id == device::mojom::XRDeviceId::ARCORE_DEVICE_ID; | 
|  | #endif | 
|  | #if BUILDFLAG(ENABLE_CARDBOARD) | 
|  | send_renderer_information = | 
|  | send_renderer_information || | 
|  | request.runtime_id == device::mojom::XRDeviceId::CARDBOARD_DEVICE_ID; | 
|  | #endif | 
|  | #if BUILDFLAG(ENABLE_OPENXR) && BUILDFLAG(IS_ANDROID) | 
|  | send_renderer_information = | 
|  | send_renderer_information || | 
|  | request.runtime_id == device::mojom::XRDeviceId::OPENXR_DEVICE_ID; | 
|  | #endif | 
|  | if (send_renderer_information) { | 
|  | runtime_options->render_process_id = | 
|  | render_frame_host_->GetProcess()->GetDeprecatedID(); | 
|  | runtime_options->render_frame_id = render_frame_host_->GetRoutingID(); | 
|  | } | 
|  | } | 
|  |  | 
|  | bool use_dom_overlay = | 
|  | base::Contains(runtime_options->required_features, | 
|  | device::mojom::XRSessionFeature::DOM_OVERLAY) || | 
|  | base::Contains(runtime_options->optional_features, | 
|  | device::mojom::XRSessionFeature::DOM_OVERLAY); | 
|  |  | 
|  | if (use_dom_overlay) { | 
|  | // Tell RenderFrameHostImpl that we're setting up the WebXR DOM Overlay, | 
|  | // it checks for this in EnterFullscreen via HasSeenRecentXrOverlaySetup(). | 
|  | render_frame_host_->SetIsXrOverlaySetup(); | 
|  | } | 
|  |  | 
|  | if (device::XRSessionModeUtils::IsImmersive(runtime_options->mode)) { | 
|  | if (!request.options->tracked_images.empty()) { | 
|  | DVLOG(3) << __func__ << ": request.options->tracked_images.size()=" | 
|  | << request.options->tracked_images.size(); | 
|  | runtime_options->tracked_images.resize( | 
|  | request.options->tracked_images.size()); | 
|  | for (std::size_t i = 0; i < request.options->tracked_images.size(); ++i) { | 
|  | runtime_options->tracked_images[i] = | 
|  | request.options->tracked_images[i].Clone(); | 
|  | } | 
|  | } | 
|  |  | 
|  | runtime_options->depth_options = std::move(request.options->depth_options); | 
|  |  | 
|  | auto immersive_callback = | 
|  | base::BindOnce(&VRServiceImpl::OnImmersiveSessionCreated, | 
|  | weak_ptr_factory_.GetWeakPtr(), std::move(request)); | 
|  |  | 
|  | runtime->RequestImmersiveSession(this, std::move(runtime_options), | 
|  | std::move(immersive_callback)); | 
|  | } else { | 
|  | auto non_immersive_callback = | 
|  | base::BindOnce(&VRServiceImpl::OnInlineSessionCreated, | 
|  | weak_ptr_factory_.GetWeakPtr(), std::move(request)); | 
|  | runtime->RequestInlineSession(std::move(runtime_options), | 
|  | std::move(non_immersive_callback)); | 
|  | } | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::SupportsSession( | 
|  | device::mojom::XRSessionOptionsPtr options, | 
|  | device::mojom::VRService::SupportsSessionCallback callback) { | 
|  | if (!initialization_complete_) { | 
|  | pending_requests_.push_back( | 
|  | base::BindOnce(&VRServiceImpl::SupportsSession, base::Unretained(this), | 
|  | std::move(options), std::move(callback))); | 
|  | return; | 
|  | } | 
|  |  | 
|  | TRACE_EVENT("xr", "VRServiceImpl::SupportsSession: received", | 
|  | perfetto::Flow::Global(options->trace_id)); | 
|  |  | 
|  | runtime_manager_->SupportsSession(std::move(options), std::move(callback)); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::ExitPresent(ExitPresentCallback on_exited) { | 
|  | BrowserXRRuntimeImpl* immersive_runtime = | 
|  | runtime_manager_->GetCurrentlyPresentingImmersiveRuntime(); | 
|  | DVLOG(2) << __func__ << ": !!immersive_runtime=" << !!immersive_runtime; | 
|  | if (immersive_runtime) { | 
|  | on_exit_present_ = std::move(on_exited); | 
|  | immersive_runtime->ExitPresent(this); | 
|  | } else { | 
|  | std::move(on_exited).Run(); | 
|  | } | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::SetFramesThrottled(bool throttled) { | 
|  | if (throttled != frames_throttled_) { | 
|  | frames_throttled_ = throttled; | 
|  | BrowserXRRuntimeImpl* immersive_runtime = | 
|  | runtime_manager_->GetCurrentlyPresentingImmersiveRuntime(); | 
|  | if (immersive_runtime) { | 
|  | immersive_runtime->SetFramesThrottled(this, frames_throttled_); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::MakeXrCompatible( | 
|  | device::mojom::VRService::MakeXrCompatibleCallback callback) { | 
|  | if (!initialization_complete_) { | 
|  | pending_requests_.push_back(base::BindOnce(&VRServiceImpl::MakeXrCompatible, | 
|  | base::Unretained(this), | 
|  | std::move(callback))); | 
|  | return; | 
|  | } | 
|  |  | 
|  | xr_compatible_callbacks_.emplace_back(std::move(callback)); | 
|  |  | 
|  | // Only request compatibility if there aren't any pending calls. | 
|  | // OnMakeXrCompatibleComplete will run all callbacks. | 
|  | if (xr_compatible_callbacks_.size() == 1) | 
|  | runtime_manager_->MakeXrCompatible(); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::OnMakeXrCompatibleComplete( | 
|  | device::mojom::XrCompatibleResult result) { | 
|  | for (XrCompatibleCallback& wrapper : xr_compatible_callbacks_) | 
|  | std::move(wrapper.callback).Run(result); | 
|  |  | 
|  | xr_compatible_callbacks_.clear(); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::OnExitPresent() { | 
|  | DVLOG(2) << __func__; | 
|  |  | 
|  | if (render_frame_host_) { | 
|  | // Clear any XrRenderTarget that may have been set. | 
|  | viz::FrameSinkId default_frame_sink_id; | 
|  | static_cast<WebContentsImpl*>(GetWebContents()) | 
|  | ->OnXrHasRenderTarget(default_frame_sink_id); | 
|  |  | 
|  | if (has_immersive_session_) { | 
|  | render_frame_host_->GetProcess()->OnImmersiveXrSessionStopped(); | 
|  | GetSessionMetricsHelper()->StopAndRecordImmersiveSession(); | 
|  | has_immersive_session_ = false; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (on_exit_present_) { | 
|  | std::move(on_exit_present_).Run(); | 
|  | } | 
|  |  | 
|  | for (auto& client : session_clients_) { | 
|  | // https://crbug.com/1160940 has a fairly generic callstack, in mojom | 
|  | // generated code, which appears to aggregate a few different actual crashes | 
|  | // into the same bug. For the crashes that appear to be our fault, the | 
|  | // common "start" is this call. By causing a CHECK here instead of in the | 
|  | // mojom generated code, we can isolate our crashes. | 
|  | CHECK(client); | 
|  | client->OnExitPresent(); | 
|  | } | 
|  |  | 
|  | // Ensure that the client list is erased to avoid "Cannot issue Interface | 
|  | // method calls on an unbound Remote" errors: https://crbug.com/991747 | 
|  | session_clients_.Clear(); | 
|  | } | 
|  |  | 
|  | void VRServiceImpl::OnVisibilityStateChanged( | 
|  | device::mojom::XRVisibilityState visiblity_state) { | 
|  | visibility_state_ = visiblity_state; | 
|  | for (auto& client : session_clients_) | 
|  | client->OnVisibilityStateChanged(visiblity_state); | 
|  | } | 
|  |  | 
|  | content::WebContents* VRServiceImpl::GetWebContents() { | 
|  | return content::WebContents::FromRenderFrameHost(render_frame_host_); | 
|  | } | 
|  |  | 
|  | }  // namespace content |