diff options
Diffstat (limited to 'chromium/third_party/blink/renderer/modules/xr/xr_system.cc')
-rw-r--r-- | chromium/third_party/blink/renderer/modules/xr/xr_system.cc | 1410 |
1 files changed, 1410 insertions, 0 deletions
diff --git a/chromium/third_party/blink/renderer/modules/xr/xr_system.cc b/chromium/third_party/blink/renderer/modules/xr/xr_system.cc new file mode 100644 index 00000000000..a94164360dc --- /dev/null +++ b/chromium/third_party/blink/renderer/modules/xr/xr_system.cc @@ -0,0 +1,1410 @@ +// Copyright 2017 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 "third_party/blink/renderer/modules/xr/xr_system.h" + +#include <utility> + +#include "services/metrics/public/cpp/ukm_builders.h" +#include "third_party/blink/public/common/browser_interface_broker_proxy.h" +#include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom-blink.h" +#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" +#include "third_party/blink/renderer/bindings/core/v8/v8_fullscreen_options.h" +#include "third_party/blink/renderer/core/dom/document.h" +#include "third_party/blink/renderer/core/dom/dom_exception.h" +#include "third_party/blink/renderer/core/dom/element.h" +#include "third_party/blink/renderer/core/frame/local_frame.h" +#include "third_party/blink/renderer/core/fullscreen/fullscreen.h" +#include "third_party/blink/renderer/core/fullscreen/scoped_allow_fullscreen.h" +#include "third_party/blink/renderer/core/html/html_element.h" +#include "third_party/blink/renderer/core/inspector/console_message.h" +#include "third_party/blink/renderer/core/layout/layout_view.h" +#include "third_party/blink/renderer/core/loader/document_loader.h" +#include "third_party/blink/renderer/modules/event_modules.h" +#include "third_party/blink/renderer/modules/event_target_modules.h" +#include "third_party/blink/renderer/modules/xr/xr_frame_provider.h" +#include "third_party/blink/renderer/modules/xr/xr_session.h" +#include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h" +#include "third_party/blink/renderer/platform/heap/heap.h" +#include "third_party/blink/renderer/platform/wtf/functional.h" +#include "third_party/blink/renderer/platform/wtf/text/string_view.h" +#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" + +namespace blink { + +namespace { + +const char kNavigatorDetachedError[] = + "The navigator.xr object is no longer associated with a document."; + +const char kPageNotVisible[] = "The page is not visible"; + +const char kFeaturePolicyBlocked[] = + "Access to the feature \"xr\" is disallowed by feature policy."; + +const char kActiveImmersiveSession[] = + "There is already an active, immersive XRSession."; + +const char kRequestRequiresUserActivation[] = + "The requested session requires user activation."; + +const char kSessionNotSupported[] = + "The specified session configuration is not supported."; + +const char kNoDevicesMessage[] = "No XR hardware found."; + +const char kImmersiveArModeNotValid[] = + "Failed to execute '%s' on 'XRSystem': The provided value 'immersive-ar' " + "is not a valid enum value of type XRSessionMode."; + +constexpr device::mojom::XRSessionFeature kDefaultImmersiveVrFeatures[] = { + device::mojom::XRSessionFeature::REF_SPACE_VIEWER, + device::mojom::XRSessionFeature::REF_SPACE_LOCAL, +}; + +constexpr device::mojom::XRSessionFeature kDefaultImmersiveArFeatures[] = { + device::mojom::XRSessionFeature::REF_SPACE_VIEWER, + device::mojom::XRSessionFeature::REF_SPACE_LOCAL, +}; + +constexpr device::mojom::XRSessionFeature kDefaultInlineFeatures[] = { + device::mojom::XRSessionFeature::REF_SPACE_VIEWER, +}; + +device::mojom::blink::XRSessionMode stringToSessionMode( + const String& mode_string) { + if (mode_string == "inline") { + return device::mojom::blink::XRSessionMode::kInline; + } + if (mode_string == "immersive-vr") { + return device::mojom::blink::XRSessionMode::kImmersiveVr; + } + if (mode_string == "immersive-ar") { + return device::mojom::blink::XRSessionMode::kImmersiveAr; + } + + NOTREACHED(); // Only strings in the enum are allowed by IDL. + return device::mojom::blink::XRSessionMode::kInline; +} + +const char* SessionModeToString(device::mojom::blink::XRSessionMode mode) { + switch (mode) { + case device::mojom::blink::XRSessionMode::kInline: + return "inline"; + case device::mojom::blink::XRSessionMode::kImmersiveVr: + return "immersive-vr"; + case device::mojom::blink::XRSessionMode::kImmersiveAr: + return "immersive-ar"; + } + + NOTREACHED(); + return ""; +} + +// Converts the given string to an XRSessionFeature. If the string is +// unrecognized, returns nullopt. Based on the spec: +// https://immersive-web.github.io/webxr/#feature-name +base::Optional<device::mojom::XRSessionFeature> StringToXRSessionFeature( + const Document* doc, + const String& feature_string) { + if (feature_string == "viewer") { + return device::mojom::XRSessionFeature::REF_SPACE_VIEWER; + } else if (feature_string == "local") { + return device::mojom::XRSessionFeature::REF_SPACE_LOCAL; + } else if (feature_string == "local-floor") { + return device::mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR; + } else if (feature_string == "bounded-floor") { + return device::mojom::XRSessionFeature::REF_SPACE_BOUNDED_FLOOR; + } else if (feature_string == "unbounded") { + return device::mojom::XRSessionFeature::REF_SPACE_UNBOUNDED; + } else if (RuntimeEnabledFeatures::WebXRHitTestEnabled(doc) && + feature_string == "hit-test") { + return device::mojom::XRSessionFeature::HIT_TEST; + } else if (feature_string == "dom-overlay") { + return device::mojom::XRSessionFeature::DOM_OVERLAY; + } + + return base::nullopt; +} + +bool IsFeatureValidForMode(device::mojom::XRSessionFeature feature, + device::mojom::blink::XRSessionMode mode, + XRSessionInit* session_init, + ExecutionContext* execution_context, + mojom::ConsoleMessageLevel error_level) { + switch (feature) { + case device::mojom::XRSessionFeature::REF_SPACE_VIEWER: + case device::mojom::XRSessionFeature::REF_SPACE_LOCAL: + case device::mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR: + return true; + case device::mojom::XRSessionFeature::REF_SPACE_BOUNDED_FLOOR: + case device::mojom::XRSessionFeature::REF_SPACE_UNBOUNDED: + case device::mojom::XRSessionFeature::HIT_TEST: + return mode == device::mojom::blink::XRSessionMode::kImmersiveVr || + mode == device::mojom::blink::XRSessionMode::kImmersiveAr; + case device::mojom::XRSessionFeature::DOM_OVERLAY: + if (mode != device::mojom::blink::XRSessionMode::kImmersiveAr) + return false; + if (!session_init->hasDomOverlay()) { + execution_context->AddConsoleMessage(MakeGarbageCollected< + ConsoleMessage>( + mojom::ConsoleMessageSource::kJavaScript, error_level, + "Must specify a valid domOverlay.root element in XRSessionInit")); + return false; + } + return true; + } +} + +bool HasRequiredFeaturePolicy(const Document* doc, + device::mojom::XRSessionFeature feature) { + if (!doc) + return false; + + switch (feature) { + case device::mojom::XRSessionFeature::REF_SPACE_VIEWER: + return true; + case device::mojom::XRSessionFeature::REF_SPACE_LOCAL: + case device::mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR: + case device::mojom::XRSessionFeature::REF_SPACE_BOUNDED_FLOOR: + case device::mojom::XRSessionFeature::REF_SPACE_UNBOUNDED: + case device::mojom::XRSessionFeature::DOM_OVERLAY: + case device::mojom::XRSessionFeature::HIT_TEST: + return doc->IsFeatureEnabled(mojom::blink::FeaturePolicyFeature::kWebXr, + ReportOptions::kReportOnFailure); + } +} + +// Ensure that the immersive session request is allowed, if not +// return which security error occurred. +// https://immersive-web.github.io/webxr/#immersive-session-request-is-allowed +const char* CheckImmersiveSessionRequestAllowed(LocalFrame* frame, + Document* doc) { + // Ensure that the session was initiated by a user gesture + if (!LocalFrame::HasTransientUserActivation(frame)) { + return kRequestRequiresUserActivation; + } + + // Check that the document is "trustworthy" + // https://immersive-web.github.io/webxr/#trustworthy + if (!doc->IsPageVisible()) { + return kPageNotVisible; + } + + // Consent occurs in the Browser process. + + return nullptr; +} + +} // namespace + +// Ensure that the inline session request is allowed, if not +// return which security error occurred. +// https://immersive-web.github.io/webxr/#inline-session-request-is-allowed +const char* XRSystem::CheckInlineSessionRequestAllowed( + LocalFrame* frame, + const PendingRequestSessionQuery& query) { + // Without user activation, we must reject the session if *any* features + // (optional or required) were present, whether or not they were recognized. + // The only exception to this is the 'viewer' feature. + if (!LocalFrame::HasTransientUserActivation(frame)) { + if (query.InvalidOptionalFeatures() || query.InvalidRequiredFeatures()) { + return kRequestRequiresUserActivation; + } + + // If any required features (besides 'viewer') were requested, reject. + for (auto feature : query.RequiredFeatures()) { + if (feature != device::mojom::XRSessionFeature::REF_SPACE_VIEWER) { + return kRequestRequiresUserActivation; + } + } + + // If any optional features (besides 'viewer') were requested, reject. + for (auto feature : query.OptionalFeatures()) { + if (feature != device::mojom::XRSessionFeature::REF_SPACE_VIEWER) { + return kRequestRequiresUserActivation; + } + } + } + + return nullptr; +} + +XRSystem::PendingSupportsSessionQuery::PendingSupportsSessionQuery( + ScriptPromiseResolver* resolver, + device::mojom::blink::XRSessionMode session_mode, + bool throw_on_unsupported) + : resolver_(resolver), + mode_(session_mode), + throw_on_unsupported_(throw_on_unsupported) {} + +void XRSystem::PendingSupportsSessionQuery::Trace(Visitor* visitor) { + visitor->Trace(resolver_); +} + +void XRSystem::PendingSupportsSessionQuery::Resolve( + bool supported, + ExceptionState* exception_state) { + if (throw_on_unsupported_) { + if (supported) { + resolver_->Resolve(); + } else { + DVLOG(2) << __func__ << ": session is unsupported - throwing exception"; + RejectWithDOMException(DOMExceptionCode::kNotSupportedError, + kSessionNotSupported, exception_state); + } + } else { + resolver_->Resolve(supported); + } +} + +void XRSystem::PendingSupportsSessionQuery::RejectWithDOMException( + DOMExceptionCode exception_code, + const String& message, + ExceptionState* exception_state) { + DCHECK_NE(exception_code, DOMExceptionCode::kSecurityError); + + if (exception_state) { + // The generated bindings will reject the returned promise for us. + // Detaching the resolver prevents it from thinking we abandoned + // the promise. + exception_state->ThrowDOMException(exception_code, message); + resolver_->Detach(); + } else { + resolver_->Reject( + MakeGarbageCollected<DOMException>(exception_code, message)); + } +} + +void XRSystem::PendingSupportsSessionQuery::RejectWithSecurityError( + const String& sanitized_message, + ExceptionState* exception_state) { + if (exception_state) { + // The generated V8 bindings will reject the returned promise for us. + // Detaching the resolver prevents it from thinking we abandoned + // the promise. + exception_state->ThrowSecurityError(sanitized_message); + resolver_->Detach(); + } else { + resolver_->Reject(MakeGarbageCollected<DOMException>( + DOMExceptionCode::kSecurityError, sanitized_message)); + } +} + +void XRSystem::PendingSupportsSessionQuery::RejectWithTypeError( + const String& message, + ExceptionState* exception_state) { + if (exception_state) { + // The generated bindings will reject the returned promise for us. + // Detaching the resolver prevents it from thinking we abandoned + // the promise. + exception_state->ThrowTypeError(message); + resolver_->Detach(); + } else { + resolver_->Reject(V8ThrowException::CreateTypeError( + resolver_->GetScriptState()->GetIsolate(), message)); + } +} + +device::mojom::blink::XRSessionMode +XRSystem::PendingSupportsSessionQuery::mode() const { + return mode_; +} + +XRSystem::PendingRequestSessionQuery::PendingRequestSessionQuery( + int64_t ukm_source_id, + ScriptPromiseResolver* resolver, + device::mojom::blink::XRSessionMode session_mode, + RequestedXRSessionFeatureSet required_features, + RequestedXRSessionFeatureSet optional_features) + : resolver_(resolver), + mode_(session_mode), + required_features_(std::move(required_features)), + optional_features_(std::move(optional_features)), + ukm_source_id_(ukm_source_id) { + ParseSensorRequirement(); +} + +void XRSystem::PendingRequestSessionQuery::Resolve( + XRSession* session, + mojo::PendingRemote<device::mojom::blink::XRSessionMetricsRecorder> + metrics_recorder) { + resolver_->Resolve(session); + ReportRequestSessionResult(SessionRequestStatus::kSuccess, session, + std::move(metrics_recorder)); +} + +void XRSystem::PendingRequestSessionQuery::RejectWithDOMException( + DOMExceptionCode exception_code, + const String& message, + ExceptionState* exception_state) { + DCHECK_NE(exception_code, DOMExceptionCode::kSecurityError); + + if (exception_state) { + exception_state->ThrowDOMException(exception_code, message); + resolver_->Detach(); + } else { + resolver_->Reject( + MakeGarbageCollected<DOMException>(exception_code, message)); + } + + ReportRequestSessionResult(SessionRequestStatus::kOtherError); +} + +void XRSystem::PendingRequestSessionQuery::RejectWithSecurityError( + const String& sanitized_message, + ExceptionState* exception_state) { + if (exception_state) { + exception_state->ThrowSecurityError(sanitized_message); + resolver_->Detach(); + } else { + resolver_->Reject(MakeGarbageCollected<DOMException>( + DOMExceptionCode::kSecurityError, sanitized_message)); + } + + ReportRequestSessionResult(SessionRequestStatus::kOtherError); +} + +void XRSystem::PendingRequestSessionQuery::RejectWithTypeError( + const String& message, + ExceptionState* exception_state) { + if (exception_state) { + exception_state->ThrowTypeError(message); + resolver_->Detach(); + } else { + resolver_->Reject(V8ThrowException::CreateTypeError( + GetScriptState()->GetIsolate(), message)); + } + + ReportRequestSessionResult(SessionRequestStatus::kOtherError); +} + +device::mojom::XRSessionFeatureRequestStatus +XRSystem::PendingRequestSessionQuery::GetFeatureRequestStatus( + device::mojom::XRSessionFeature feature, + const XRSession* session) const { + using device::mojom::XRSessionFeatureRequestStatus; + + if (RequiredFeatures().Contains(feature)) { + // In the case of required features, accepted/rejected state is + // the same as the entire session. + return XRSessionFeatureRequestStatus::kRequired; + } + + if (OptionalFeatures().Contains(feature)) { + if (!session || !session->IsFeatureEnabled(feature)) { + return XRSessionFeatureRequestStatus::kOptionalRejected; + } + + return XRSessionFeatureRequestStatus::kOptionalAccepted; + } + + return XRSessionFeatureRequestStatus::kNotRequested; +} + +void XRSystem::PendingRequestSessionQuery::ReportRequestSessionResult( + SessionRequestStatus status, + XRSession* session, + mojo::PendingRemote<device::mojom::blink::XRSessionMetricsRecorder> + metrics_recorder) { + using device::mojom::XRSessionFeature; + + LocalFrame* frame = resolver_->GetFrame(); + Document* doc = frame ? frame->GetDocument() : nullptr; + if (!doc) + return; + + auto feature_request_viewer = + GetFeatureRequestStatus(XRSessionFeature::REF_SPACE_VIEWER, session); + auto feature_request_local = + GetFeatureRequestStatus(XRSessionFeature::REF_SPACE_LOCAL, session); + auto feature_request_local_floor = + GetFeatureRequestStatus(XRSessionFeature::REF_SPACE_LOCAL_FLOOR, session); + auto feature_request_bounded_floor = GetFeatureRequestStatus( + XRSessionFeature::REF_SPACE_BOUNDED_FLOOR, session); + auto feature_request_unbounded = + GetFeatureRequestStatus(XRSessionFeature::REF_SPACE_UNBOUNDED, session); + auto feature_request_dom_overlay = + GetFeatureRequestStatus(XRSessionFeature::DOM_OVERLAY, session); + + ukm::builders::XR_WebXR_SessionRequest(ukm_source_id_) + .SetMode(static_cast<int64_t>(mode_)) + .SetStatus(static_cast<int64_t>(status)) + .SetFeature_Viewer(static_cast<int64_t>(feature_request_viewer)) + .SetFeature_Local(static_cast<int64_t>(feature_request_local)) + .SetFeature_LocalFloor(static_cast<int64_t>(feature_request_local_floor)) + .SetFeature_BoundedFloor( + static_cast<int64_t>(feature_request_bounded_floor)) + .SetFeature_Unbounded(static_cast<int64_t>(feature_request_unbounded)) + .Record(doc->UkmRecorder()); + + // If the session was successfully created and DOM overlay was requested, + // count this as a use of the DOM overlay feature. + if (session && status == SessionRequestStatus::kSuccess && + feature_request_dom_overlay != + device::mojom::XRSessionFeatureRequestStatus::kNotRequested) { + UseCounter::Count(session->GetExecutionContext(), + WebFeature::kXRDOMOverlay); + } + + if (session && metrics_recorder) { + mojo::Remote<device::mojom::blink::XRSessionMetricsRecorder> recorder( + std::move(metrics_recorder)); + session->SetMetricsReporter( + std::make_unique<XRSession::MetricsReporter>(std::move(recorder))); + } +} + +device::mojom::blink::XRSessionMode XRSystem::PendingRequestSessionQuery::mode() + const { + return mode_; +} + +const XRSessionFeatureSet& +XRSystem::PendingRequestSessionQuery::RequiredFeatures() const { + return required_features_.valid_features; +} + +const XRSessionFeatureSet& +XRSystem::PendingRequestSessionQuery::OptionalFeatures() const { + return optional_features_.valid_features; +} + +bool XRSystem::PendingRequestSessionQuery::InvalidRequiredFeatures() const { + return required_features_.invalid_features; +} + +bool XRSystem::PendingRequestSessionQuery::InvalidOptionalFeatures() const { + return optional_features_.invalid_features; +} + +ScriptState* XRSystem::PendingRequestSessionQuery::GetScriptState() const { + return resolver_->GetScriptState(); +} + +void XRSystem::PendingRequestSessionQuery::ParseSensorRequirement() { + // All modes other than inline require sensors. + if (mode_ != device::mojom::blink::XRSessionMode::kInline) { + sensor_requirement_ = SensorRequirement::kRequired; + return; + } + + // If any required features require sensors, then sensors are required. + for (const auto& feature : RequiredFeatures()) { + if (feature != device::mojom::XRSessionFeature::REF_SPACE_VIEWER) { + sensor_requirement_ = SensorRequirement::kRequired; + return; + } + } + + // If any optional features require sensors, then sensors are optional. + for (const auto& feature : OptionalFeatures()) { + if (feature != device::mojom::XRSessionFeature::REF_SPACE_VIEWER) { + sensor_requirement_ = SensorRequirement::kOptional; + return; + } + } + + // By this point any situation that requires sensors should have returned. + sensor_requirement_ = kNone; +} + +void XRSystem::PendingRequestSessionQuery::Trace(Visitor* visitor) { + visitor->Trace(resolver_); + visitor->Trace(dom_overlay_element_); +} + +XRSystem::OverlayFullscreenEventManager::OverlayFullscreenEventManager( + XRSystem* xr, + XRSystem::PendingRequestSessionQuery* query, + device::mojom::blink::RequestSessionResultPtr result) + : xr_(xr), query_(query), result_(std::move(result)) { + DVLOG(2) << __func__; +} + +XRSystem::OverlayFullscreenEventManager::~OverlayFullscreenEventManager() = + default; + +void XRSystem::OverlayFullscreenEventManager::Invoke( + ExecutionContext* execution_context, + Event* event) { + DVLOG(2) << __func__ << ": event type=" << event->type(); + + // This handler should only be called once, it's unregistered after use. + DCHECK(query_); + DCHECK(result_); + + Element* element = query_->DOMOverlayElement(); + element->GetDocument().removeEventListener( + event_type_names::kFullscreenchange, this, true); + element->GetDocument().removeEventListener(event_type_names::kFullscreenerror, + this, true); + + if (event->type() == event_type_names::kFullscreenchange) { + // Succeeded, proceed with session creation. + element->GetDocument().SetIsXrOverlay(true, element); + xr_->OnRequestSessionReturned(query_, std::move(result_)); + } + + if (event->type() == event_type_names::kFullscreenerror) { + // Failed, reject the session + xr_->OnRequestSessionReturned( + query_, device::mojom::blink::RequestSessionResult::NewFailureReason( + device::mojom::RequestSessionError::INVALID_CLIENT)); + } +} + +void XRSystem::OverlayFullscreenEventManager::RequestFullscreen() { + Element* element = query_->DOMOverlayElement(); + DCHECK(element); + + if (element == Fullscreen::FullscreenElementFrom(element->GetDocument())) { + // It's possible that the requested element is already fullscreen, in which + // case we must not wait for a fullscreenchange event since it won't arrive. + // Detect that and proceed directly with session creation in this case. This + // can happen if the site used Fullscreen API to place the element into + // fullscreen mode before requesting the session, and if the session can + // proceed without needing a consent prompt. (Showing a dialog exits + // fullscreen mode.) + DVLOG(2) << __func__ << ": requested element already fullscreen"; + element->GetDocument().SetIsXrOverlay(true, element); + xr_->OnRequestSessionReturned(query_, std::move(result_)); + return; + } + + // Set up event listeners for success and failure. + element->GetDocument().addEventListener(event_type_names::kFullscreenchange, + this, true); + element->GetDocument().addEventListener(event_type_names::kFullscreenerror, + this, true); + + // Use the event-generating unprefixed version of RequestFullscreen to ensure + // that the fullscreen event listener is informed once this completes. + FullscreenOptions* options = FullscreenOptions::Create(); + options->setNavigationUI("hide"); + + // Grant fullscreen API permission for the following call. Requesting the + // immersive session had required a user activation state, but that may have + // expired by now due to the user taking time to respond to the consent + // prompt. + ScopedAllowFullscreen scope(ScopedAllowFullscreen::kXrOverlay); + + Fullscreen::RequestFullscreen(*element, options, + Fullscreen::RequestType::kUnprefixed); +} + +void XRSystem::OverlayFullscreenEventManager::Trace(Visitor* visitor) { + visitor->Trace(xr_); + visitor->Trace(query_); + EventListener::Trace(visitor); +} + +XRSystem::OverlayFullscreenExitObserver::OverlayFullscreenExitObserver( + XRSystem* xr) + : xr_(xr) { + DVLOG(2) << __func__; +} + +XRSystem::OverlayFullscreenExitObserver::~OverlayFullscreenExitObserver() = + default; + +void XRSystem::OverlayFullscreenExitObserver::Invoke( + ExecutionContext* execution_context, + Event* event) { + DVLOG(2) << __func__ << ": event type=" << event->type(); + + element_->GetDocument().removeEventListener( + event_type_names::kFullscreenchange, this, true); + + if (event->type() == event_type_names::kFullscreenchange) { + // Succeeded, proceed with session shutdown. + xr_->ExitPresent(std::move(on_exited_)); + } +} + +void XRSystem::OverlayFullscreenExitObserver::ExitFullscreen( + Element* element, + base::OnceClosure on_exited) { + DVLOG(2) << __func__; + element_ = element; + on_exited_ = std::move(on_exited); + + element->GetDocument().addEventListener(event_type_names::kFullscreenchange, + this, true); + // "ua_originated" means that the browser process already exited + // fullscreen. Set it to false because we need the browser process + // to get notified that it needs to exit fullscreen. Use + // FullyExitFullscreen to ensure that we return to non-fullscreen mode. + // ExitFullscreen only unfullscreens a single element, potentially + // leaving others in fullscreen mode. + constexpr bool kUaOriginated = false; + + Fullscreen::FullyExitFullscreen(element_->GetDocument(), kUaOriginated); +} + +void XRSystem::OverlayFullscreenExitObserver::Trace(Visitor* visitor) { + visitor->Trace(xr_); + visitor->Trace(element_); + EventListener::Trace(visitor); +} + +device::mojom::blink::XRSessionOptionsPtr XRSystem::XRSessionOptionsFromQuery( + const PendingRequestSessionQuery& query) { + device::mojom::blink::XRSessionOptionsPtr session_options = + device::mojom::blink::XRSessionOptions::New(); + session_options->mode = query.mode(); + + CopyToVector(query.RequiredFeatures(), session_options->required_features); + CopyToVector(query.OptionalFeatures(), session_options->optional_features); + + return session_options; +} + +XRSystem::XRSystem(LocalFrame& frame, int64_t ukm_source_id) + : ExecutionContextLifecycleObserver(frame.GetDocument()), + FocusChangedObserver(frame.GetPage()), + ukm_source_id_(ukm_source_id), + navigation_start_( + frame.Loader().GetDocumentLoader()->GetTiming().NavigationStart()), + feature_handle_for_scheduler_(frame.GetFrameScheduler()->RegisterFeature( + SchedulingPolicy::Feature::kWebXR, + {SchedulingPolicy::RecordMetricsForBackForwardCache()})) { + // See https://bit.ly/2S0zRAS for task types. + DCHECK(frame.IsAttached()); + frame.GetBrowserInterfaceBroker().GetInterface( + service_.BindNewPipeAndPassReceiver( + frame.GetTaskRunner(TaskType::kMiscPlatformAPI))); + service_.set_disconnect_handler(WTF::Bind(&XRSystem::Dispose, + WrapWeakPersistent(this), + DisposeType::kDisconnected)); +} + +void XRSystem::FocusedFrameChanged() { + // Tell all sessions that focus changed. + for (const auto& session : sessions_) { + session->OnFocusChanged(); + } + + if (frame_provider_) + frame_provider_->OnFocusChanged(); +} + +bool XRSystem::IsFrameFocused() { + return FocusChangedObserver::IsFrameFocused(GetFrame()); +} + +ExecutionContext* XRSystem::GetExecutionContext() const { + return ExecutionContextLifecycleObserver::GetExecutionContext(); +} + +const AtomicString& XRSystem::InterfaceName() const { + return event_target_names::kXR; +} + +XRFrameProvider* XRSystem::frameProvider() { + if (!frame_provider_) { + frame_provider_ = MakeGarbageCollected<XRFrameProvider>(this); + } + + return frame_provider_; +} + +const mojo::AssociatedRemote< + device::mojom::blink::XREnvironmentIntegrationProvider>& +XRSystem::xrEnvironmentProviderRemote() { + return environment_provider_; +} + +void XRSystem::AddEnvironmentProviderErrorHandler( + EnvironmentProviderErrorCallback callback) { + environment_provider_error_callbacks_.push_back(std::move(callback)); +} + +void XRSystem::ExitPresent(base::OnceClosure on_exited) { + DVLOG(1) << __func__; + + // If the document was potentially being shown in a DOM overlay via + // fullscreened elements, make sure to clear any fullscreen states on exiting + // the session. This avoids a race condition: + // - browser side ends session and exits fullscreen (i.e. back button) + // - renderer processes WebViewImpl::ExitFullscreen via ChromeClient + // - JS application sets a new element to fullscreen, this is allowed + // because doc->IsXrOverlay() is still true at this point + // - renderer processes XR session shutdown (this method) + // - browser re-enters fullscreen unexpectedly + LocalFrame* frame = GetFrame(); + if (frame) { + Document* doc = frame->GetDocument(); + DCHECK(doc); + DVLOG(3) << __func__ << ": doc->IsXrOverlay()=" << doc->IsXrOverlay(); + if (doc->IsXrOverlay()) { + Element* fullscreen_element = Fullscreen::FullscreenElementFrom(*doc); + DVLOG(3) << __func__ << ": fullscreen_element=" << fullscreen_element; + doc->SetIsXrOverlay(false, fullscreen_element); + + // Restore the FrameView background color that was changed in + // OnRequestSessionReturned. The layout view can be null on navigation. + auto* layout_view = doc->GetLayoutView(); + if (layout_view) { + auto* frame_view = layout_view->GetFrameView(); + // SetBaseBackgroundColor updates composited layer mappings. + // That DCHECKs IsAllowedToQueryCompositingState which requires + // DocumentLifecycle >= kInCompositingUpdate. + frame_view->UpdateLifecycleToCompositingInputsClean( + DocumentUpdateReason::kBaseColor); + frame_view->SetBaseBackgroundColor(original_base_background_color_); + } + + if (fullscreen_element) { + fullscreen_exit_observer_ = + MakeGarbageCollected<OverlayFullscreenExitObserver>(this); + fullscreen_exit_observer_->ExitFullscreen(fullscreen_element, + std::move(on_exited)); + return; + } + } + } + + if (service_) { + service_->ExitPresent(std::move(on_exited)); + } else { + // The service was already shut down, run the callback immediately. + std::move(on_exited).Run(); + } +} + +void XRSystem::SetFramesThrottled(const XRSession* session, bool throttled) { + // The service only cares if the immersive session is throttling frames. + if (session->immersive()) { + // If we have an immersive session, we should have a service. + DCHECK(service_); + service_->SetFramesThrottled(throttled); + } +} + +ScriptPromise XRSystem::supportsSession(ScriptState* script_state, + const String& mode, + ExceptionState& exception_state) { + return InternalIsSessionSupported(script_state, mode, exception_state, true); +} + +ScriptPromise XRSystem::isSessionSupported(ScriptState* script_state, + const String& mode, + ExceptionState& exception_state) { + return InternalIsSessionSupported(script_state, mode, exception_state, false); +} + +ScriptPromise XRSystem::InternalIsSessionSupported( + ScriptState* script_state, + const String& mode, + ExceptionState& exception_state, + bool throw_on_unsupported) { + LocalFrame* frame = GetFrame(); + Document* doc = frame ? frame->GetDocument() : nullptr; + if (!doc) { + // Reject if the frame or document is inaccessible. + exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, + kNavigatorDetachedError); + return ScriptPromise(); // Will be rejected by generated bindings + } + + auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); + ScriptPromise promise = resolver->Promise(); + + device::mojom::blink::XRSessionMode session_mode = stringToSessionMode(mode); + PendingSupportsSessionQuery* query = + MakeGarbageCollected<PendingSupportsSessionQuery>(resolver, session_mode, + throw_on_unsupported); + + if (session_mode == device::mojom::blink::XRSessionMode::kImmersiveAr && + !RuntimeEnabledFeatures::WebXRARModuleEnabled(doc)) { + DVLOG(2) << __func__ + << ": Immersive AR session is only supported if WebXRARModule " + "feature is enabled"; + query->Resolve(false); + return promise; + } + + if (session_mode == device::mojom::blink::XRSessionMode::kInline) { + // inline sessions are always supported. + query->Resolve(true); + return promise; + } + + if (!doc->IsFeatureEnabled(mojom::blink::FeaturePolicyFeature::kWebXr, + ReportOptions::kReportOnFailure)) { + // Only allow the call to be made if the appropriate feature policy is in + // place. + query->RejectWithSecurityError(kFeaturePolicyBlocked, &exception_state); + return promise; + } + + if (!service_) { + // If we don't have a service at the time we reach this call it indicates + // that there's no WebXR hardware. Reject as not supported. + query->Resolve(false, &exception_state); + return promise; + } + + device::mojom::blink::XRSessionOptionsPtr session_options = + device::mojom::blink::XRSessionOptions::New(); + session_options->mode = query->mode(); + + outstanding_support_queries_.insert(query); + service_->SupportsSession( + std::move(session_options), + WTF::Bind(&XRSystem::OnSupportsSessionReturned, WrapPersistent(this), + WrapPersistent(query))); + + return promise; +} + +void XRSystem::RequestImmersiveSession(LocalFrame* frame, + Document* doc, + PendingRequestSessionQuery* query, + ExceptionState* exception_state) { + DVLOG(2) << __func__; + // Log an immersive session request if we haven't already + if (!did_log_request_immersive_session_) { + ukm::builders::XR_WebXR(GetSourceId()) + .SetDidRequestPresentation(1) + .Record(doc->UkmRecorder()); + did_log_request_immersive_session_ = true; + } + + // Make sure the request is allowed + auto* immersive_session_request_error = + CheckImmersiveSessionRequestAllowed(frame, doc); + if (immersive_session_request_error) { + query->RejectWithSecurityError(immersive_session_request_error, + exception_state); + return; + } + + // Ensure there are no other immersive sessions currently pending or active + if (has_outstanding_immersive_request_ || + frameProvider()->immersive_session()) { + query->RejectWithDOMException(DOMExceptionCode::kInvalidStateError, + kActiveImmersiveSession, exception_state); + return; + } + + // If we don't have a service by the time we reach this call, there is no XR + // hardware. + if (!service_) { + query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError, + kNoDevicesMessage, exception_state); + return; + } + + // Reject session if any of the required features were invalid. + if (query->InvalidRequiredFeatures()) { + DVLOG(2) << __func__ << ": rejecting session - invalid required features"; + query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError, + kSessionNotSupported, exception_state); + return; + } + + // Reworded from spec 'pending immersive session' + has_outstanding_immersive_request_ = true; + + // Submit the request to VrServiceImpl in the Browser process + outstanding_request_queries_.insert(query); + auto session_options = XRSessionOptionsFromQuery(*query); + + // In DOM overlay mode, there's an additional step before an immersive-ar + // session can start, we need to enter fullscreen mode by setting the + // appropriate element as fullscreen from the Renderer, then waiting for the + // browser side to send an event indicating success or failure. + auto callback = + query->DOMOverlayElement() + ? WTF::Bind(&XRSystem::OnRequestSessionSetupForDomOverlay, + WrapWeakPersistent(this), WrapPersistent(query)) + : WTF::Bind(&XRSystem::OnRequestSessionReturned, + WrapWeakPersistent(this), WrapPersistent(query)); + service_->RequestSession(std::move(session_options), std::move(callback)); +} + +void XRSystem::RequestInlineSession(LocalFrame* frame, + PendingRequestSessionQuery* query, + ExceptionState* exception_state) { + DVLOG(2) << __func__; + // Make sure the inline session request was allowed + auto* inline_session_request_error = + CheckInlineSessionRequestAllowed(frame, *query); + if (inline_session_request_error) { + query->RejectWithSecurityError(inline_session_request_error, + exception_state); + return; + } + + // Reject session if any of the required features were invalid. + if (query->InvalidRequiredFeatures()) { + query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError, + kSessionNotSupported, exception_state); + return; + } + + auto sensor_requirement = query->GetSensorRequirement(); + + // If no sensors are requested, or if we don't have a service and sensors are + // not required, then just create a sensorless session. + if (sensor_requirement == SensorRequirement::kNone || + (!service_ && sensor_requirement != SensorRequirement::kRequired)) { + query->Resolve(CreateSensorlessInlineSession()); + return; + } + + // If we don't have a service, then we don't have any WebXR hardware. + // If we didn't already create a sensorless session, we can't create a session + // without hardware, so just reject now. + if (!service_) { + query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError, + kSessionNotSupported, exception_state); + return; + } + + // Submit the request to VrServiceImpl in the Browser process + outstanding_request_queries_.insert(query); + auto session_options = XRSessionOptionsFromQuery(*query); + service_->RequestSession( + std::move(session_options), + WTF::Bind(&XRSystem::OnRequestSessionReturned, WrapWeakPersistent(this), + WrapPersistent(query))); +} + +XRSystem::RequestedXRSessionFeatureSet XRSystem::ParseRequestedFeatures( + Document* doc, + const HeapVector<ScriptValue>& features, + const device::mojom::blink::XRSessionMode& session_mode, + XRSessionInit* session_init, + mojom::ConsoleMessageLevel error_level) { + RequestedXRSessionFeatureSet result; + + // Iterate over all requested features, even if intermediate + // elements are found to be invalid. + for (const auto& feature : features) { + String feature_string; + if (feature.ToString(feature_string)) { + auto feature_enum = StringToXRSessionFeature(doc, feature_string); + + if (!feature_enum) { + GetExecutionContext()->AddConsoleMessage( + MakeGarbageCollected<ConsoleMessage>( + mojom::ConsoleMessageSource::kJavaScript, error_level, + "Unrecognized feature requested: " + feature_string)); + result.invalid_features = true; + } else if (!IsFeatureValidForMode(feature_enum.value(), session_mode, + session_init, GetExecutionContext(), + error_level)) { + GetExecutionContext()->AddConsoleMessage( + MakeGarbageCollected<ConsoleMessage>( + mojom::ConsoleMessageSource::kJavaScript, error_level, + "Feature '" + feature_string + "' is not supported for mode: " + + SessionModeToString(session_mode))); + result.invalid_features = true; + } else if (!HasRequiredFeaturePolicy(doc, feature_enum.value())) { + GetExecutionContext()->AddConsoleMessage( + MakeGarbageCollected<ConsoleMessage>( + mojom::ConsoleMessageSource::kJavaScript, error_level, + "Feature '" + feature_string + + "' is not permitted by feature policy")); + result.invalid_features = true; + } else { + result.valid_features.insert(feature_enum.value()); + } + } else { + GetExecutionContext()->AddConsoleMessage( + MakeGarbageCollected<ConsoleMessage>( + mojom::ConsoleMessageSource::kJavaScript, error_level, + "Unrecognized feature value")); + result.invalid_features = true; + } + } + + return result; +} + +ScriptPromise XRSystem::requestSession(ScriptState* script_state, + const String& mode, + XRSessionInit* session_init, + ExceptionState& exception_state) { + DVLOG(2) << __func__; + // TODO(https://crbug.com/968622): Make sure we don't forget to call + // metrics-related methods when the promise gets resolved/rejected. + LocalFrame* frame = GetFrame(); + Document* doc = frame ? frame->GetDocument() : nullptr; + if (!doc) { + // Reject if the frame or doc is inaccessible. + + // Do *not* record an UKM event in this case (we won't be able to access the + // Document to get UkmRecorder anyway). + exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, + kNavigatorDetachedError); + return ScriptPromise(); // Will be rejected by generated bindings + } + + device::mojom::blink::XRSessionMode session_mode = stringToSessionMode(mode); + + // If the request is for immersive-ar, ensure that feature is enabled. + if (session_mode == device::mojom::blink::XRSessionMode::kImmersiveAr && + !RuntimeEnabledFeatures::WebXRARModuleEnabled(doc)) { + exception_state.ThrowTypeError( + String::Format(kImmersiveArModeNotValid, "requestSession")); + + // We haven't created the query yet, so we can't use it to implicitly log + // our metrics for us, so explicitly log it here, as the query requires the + // features to be parsed before it can be built. + ukm::builders::XR_WebXR_SessionRequest(GetSourceId()) + .SetMode(static_cast<int64_t>(session_mode)) + .SetStatus(static_cast<int64_t>(SessionRequestStatus::kOtherError)) + .Record(doc->UkmRecorder()); + return ScriptPromise(); + } + + // Parse required feature strings + RequestedXRSessionFeatureSet required_features; + if (session_init && session_init->hasRequiredFeatures()) { + required_features = ParseRequestedFeatures( + doc, session_init->requiredFeatures(), session_mode, session_init, + mojom::ConsoleMessageLevel::kError); + } + + // Parse optional feature strings + RequestedXRSessionFeatureSet optional_features; + if (session_init && session_init->hasOptionalFeatures()) { + optional_features = ParseRequestedFeatures( + doc, session_init->optionalFeatures(), session_mode, session_init, + mojom::ConsoleMessageLevel::kWarning); + } + + // Certain session modes imply default features. + // Add those default features as required features now. + base::span<const device::mojom::XRSessionFeature> default_features; + switch (session_mode) { + case device::mojom::blink::XRSessionMode::kImmersiveVr: + default_features = kDefaultImmersiveVrFeatures; + break; + case device::mojom::blink::XRSessionMode::kImmersiveAr: + default_features = kDefaultImmersiveArFeatures; + break; + case device::mojom::blink::XRSessionMode::kInline: + default_features = kDefaultInlineFeatures; + break; + } + + for (const auto& feature : default_features) { + if (HasRequiredFeaturePolicy(doc, feature)) { + required_features.valid_features.insert(feature); + } else { + required_features.invalid_features = true; + } + } + + auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); + ScriptPromise promise = resolver->Promise(); + + PendingRequestSessionQuery* query = + MakeGarbageCollected<PendingRequestSessionQuery>( + GetSourceId(), resolver, session_mode, std::move(required_features), + std::move(optional_features)); + + if (session_init && session_init->hasDomOverlay()) { + DCHECK(session_init->domOverlay()->hasRoot()) << "required in IDL"; + query->SetDOMOverlayElement(session_init->domOverlay()->root()); + } + + switch (session_mode) { + case device::mojom::blink::XRSessionMode::kImmersiveVr: + case device::mojom::blink::XRSessionMode::kImmersiveAr: + RequestImmersiveSession(frame, doc, query, &exception_state); + break; + case device::mojom::blink::XRSessionMode::kInline: + RequestInlineSession(frame, query, &exception_state); + break; + } + + return promise; +} + +// This will be called when the XR hardware or capabilities have potentially +// changed. For example, if a new physical device was connected to the system, +// it might be able to support immersive sessions, where it couldn't before. +void XRSystem::OnDeviceChanged() { + LocalFrame* frame = GetFrame(); + Document* doc = frame ? frame->GetDocument() : nullptr; + if (doc && + doc->IsFeatureEnabled(mojom::blink::FeaturePolicyFeature::kWebXr)) { + DispatchEvent(*blink::Event::Create(event_type_names::kDevicechange)); + } +} + +void XRSystem::OnSupportsSessionReturned(PendingSupportsSessionQuery* query, + bool supports_session) { + // The session query has returned and we're about to resolve or reject the + // promise, so remove it from our outstanding list. + DCHECK(outstanding_support_queries_.Contains(query)); + outstanding_support_queries_.erase(query); + query->Resolve(supports_session); +} + +void XRSystem::OnRequestSessionSetupForDomOverlay( + PendingRequestSessionQuery* query, + device::mojom::blink::RequestSessionResultPtr result) { + DCHECK(query->DOMOverlayElement()); + if (result->is_success()) { + // Success. Now request fullscreen mode and continue with + // OnRequestSessionReturned once that completes. + fullscreen_event_manager_ = + MakeGarbageCollected<OverlayFullscreenEventManager>(this, query, + std::move(result)); + fullscreen_event_manager_->RequestFullscreen(); + } else { + // Session request failed, continue processing that normally. + OnRequestSessionReturned(query, std::move(result)); + } +} + +void XRSystem::OnRequestSessionReturned( + PendingRequestSessionQuery* query, + device::mojom::blink::RequestSessionResultPtr result) { + // The session query has returned and we're about to resolve or reject the + // promise, so remove it from our outstanding list. + DCHECK(outstanding_request_queries_.Contains(query)); + outstanding_request_queries_.erase(query); + if (query->mode() == device::mojom::blink::XRSessionMode::kImmersiveVr || + query->mode() == device::mojom::blink::XRSessionMode::kImmersiveAr) { + DCHECK(has_outstanding_immersive_request_); + has_outstanding_immersive_request_ = false; + } + // Clean up the fullscreen event manager which may have been added for + // DOM overlay setup. We're done with it, and it contains a reference + // to the query and the DOM overlay element. + fullscreen_event_manager_ = nullptr; + + // TODO(https://crbug.com/872316) Improve the error messaging to indicate why + // a request failed. + if (!result->is_success()) { + // |service_| does not support the requested mode. Attempt to create a + // sensorless session. + if (query->GetSensorRequirement() != SensorRequirement::kRequired) { + XRSession* session = CreateSensorlessInlineSession(); + query->Resolve(session); + return; + } + + // TODO(http://crbug.com/961960): Report appropriate exception when the user + // denies XR session request on consent dialog + // TODO(https://crbug.com/872316): Improve the error messaging to indicate + // the reason for a request failure. + query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError, + kSessionNotSupported, nullptr); + return; + } + + auto session_ptr = std::move(result->get_success()->session); + auto metrics_recorder = std::move(result->get_success()->metrics_recorder); + + bool environment_integration = + query->mode() == device::mojom::blink::XRSessionMode::kImmersiveAr; + + // immersive sessions must supply display info. + DCHECK(session_ptr->display_info); + DVLOG(2) << __func__ + << ": environment_integration=" << environment_integration; + + // TODO(https://crbug.com/944936): The blend mode could be "additive". + XRSession::EnvironmentBlendMode blend_mode = XRSession::kBlendModeOpaque; + if (environment_integration) + blend_mode = XRSession::kBlendModeAlphaBlend; + + XRSessionFeatureSet enabled_features; + for (const auto& feature : session_ptr->enabled_features) { + enabled_features.insert(feature); + } + + XRSession* session = CreateSession( + query->mode(), blend_mode, std::move(session_ptr->client_receiver), + std::move(session_ptr->display_info), session_ptr->uses_input_eventing, + enabled_features); + + frameProvider()->OnSessionStarted(session, std::move(session_ptr)); + + if (query->mode() == device::mojom::blink::XRSessionMode::kImmersiveVr || + query->mode() == device::mojom::blink::XRSessionMode::kImmersiveAr) { + if (environment_integration) { + // See Task Sources spreadsheet for more information: + // https://docs.google.com/spreadsheets/d/1b-dus1Ug3A8y0lX0blkmOjJILisUASdj8x9YN_XMwYc/view + frameProvider() + ->GetImmersiveDataProvider() + ->GetEnvironmentIntegrationProvider( + environment_provider_.BindNewEndpointAndPassReceiver( + GetExecutionContext()->GetTaskRunner( + TaskType::kMiscPlatformAPI))); + environment_provider_.set_disconnect_handler( + WTF::Bind(&XRSystem::OnEnvironmentProviderDisconnect, + WrapWeakPersistent(this))); + + session->OnEnvironmentProviderCreated(); + + LocalFrame* frame = GetFrame(); + DCHECK(frame); + + if (query->DOMOverlayElement()) { + // The session is using DOM overlay mode. At this point the overlay + // element is already in fullscreen mode, and the session can + // proceed. + Document* doc = frame->GetDocument(); + DCHECK(doc); + session->SetDOMOverlayElement(query->DOMOverlayElement()); + + // Save the current base background color (restored in ExitPresent), + // and set a transparent background for the FrameView. + auto* frame_view = doc->GetLayoutView()->GetFrameView(); + // SetBaseBackgroundColor updates composited layer mappings. + // That DCHECKs IsAllowedToQueryCompositingState which requires + // DocumentLifecycle >= kInCompositingUpdate. + frame_view->UpdateLifecycleToCompositingInputsClean( + DocumentUpdateReason::kBaseColor); + original_base_background_color_ = frame_view->BaseBackgroundColor(); + frame_view->SetBaseBackgroundColor(Color::kTransparent); + } + } + + if (query->mode() == device::mojom::blink::XRSessionMode::kImmersiveVr && + session->UsesInputEventing()) { + frameProvider()->GetImmersiveDataProvider()->SetInputSourceButtonListener( + session->GetInputClickListener()); + } + } + + UseCounter::Count(ExecutionContext::From(query->GetScriptState()), + WebFeature::kWebXrSessionCreated); + + query->Resolve(session, std::move(metrics_recorder)); +} + +void XRSystem::ReportImmersiveSupported(bool supported) { + Document* doc = GetFrame() ? GetFrame()->GetDocument() : nullptr; + if (doc && !did_log_supports_immersive_ && supported) { + ukm::builders::XR_WebXR ukm_builder(ukm_source_id_); + ukm_builder.SetReturnedPresentationCapableDevice(1); + ukm_builder.Record(doc->UkmRecorder()); + did_log_supports_immersive_ = true; + } +} + +void XRSystem::AddedEventListener( + const AtomicString& event_type, + RegisteredEventListener& registered_listener) { + EventTargetWithInlineData::AddedEventListener(event_type, + registered_listener); + + if (!service_) + return; + + if (event_type == event_type_names::kDevicechange) { + // Register for notifications if we haven't already. + // + // See https://bit.ly/2S0zRAS for task types. + auto task_runner = + GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI); + if (!receiver_.is_bound()) + service_->SetClient(receiver_.BindNewPipeAndPassRemote(task_runner)); + } +} + +void XRSystem::ContextDestroyed() { + Dispose(DisposeType::kContextDestroyed); +} + +// A session is always created and returned. +XRSession* XRSystem::CreateSession( + device::mojom::blink::XRSessionMode mode, + XRSession::EnvironmentBlendMode blend_mode, + mojo::PendingReceiver<device::mojom::blink::XRSessionClient> + client_receiver, + device::mojom::blink::VRDisplayInfoPtr display_info, + bool uses_input_eventing, + XRSessionFeatureSet enabled_features, + bool sensorless_session) { + XRSession* session = MakeGarbageCollected<XRSession>( + this, std::move(client_receiver), mode, blend_mode, uses_input_eventing, + sensorless_session, std::move(enabled_features)); + if (display_info) + session->SetXRDisplayInfo(std::move(display_info)); + sessions_.insert(session); + return session; +} + +XRSession* XRSystem::CreateSensorlessInlineSession() { + // TODO(https://crbug.com/944936): The blend mode could be "additive". + XRSession::EnvironmentBlendMode blend_mode = XRSession::kBlendModeOpaque; + return CreateSession(device::mojom::blink::XRSessionMode::kInline, blend_mode, + mojo::NullReceiver() /* client receiver */, + nullptr /* display_info */, + false /* uses_input_eventing */, + {device::mojom::XRSessionFeature::REF_SPACE_VIEWER}, + true /* sensorless_session */); +} + +void XRSystem::Dispose(DisposeType dispose_type) { + switch (dispose_type) { + case DisposeType::kContextDestroyed: + is_context_destroyed_ = true; + break; + case DisposeType::kDisconnected: + // nothing to do + break; + } + + // If the document context was destroyed, shut down the client connection + // and never call the mojo service again. + service_.reset(); + receiver_.reset(); + + // Shutdown frame provider, which manages the message pipes. + if (frame_provider_) + frame_provider_->Dispose(); + + HeapHashSet<Member<PendingSupportsSessionQuery>> support_queries = + outstanding_support_queries_; + for (const auto& query : support_queries) { + OnSupportsSessionReturned(query, false); + } + DCHECK(outstanding_support_queries_.IsEmpty()); + + HeapHashSet<Member<PendingRequestSessionQuery>> request_queries = + outstanding_request_queries_; + for (const auto& query : request_queries) { + // TODO(https://crbug.com/962991): The spec should specify + // what is returned here. + OnRequestSessionReturned( + query, device::mojom::blink::RequestSessionResult::NewFailureReason( + device::mojom::RequestSessionError::INVALID_CLIENT)); + } + DCHECK(outstanding_support_queries_.IsEmpty()); +} + +void XRSystem::OnEnvironmentProviderDisconnect() { + for (auto& callback : environment_provider_error_callbacks_) { + std::move(callback).Run(); + } + + environment_provider_error_callbacks_.clear(); + environment_provider_.reset(); +} + +void XRSystem::Trace(Visitor* visitor) { + visitor->Trace(frame_provider_); + visitor->Trace(sessions_); + visitor->Trace(outstanding_support_queries_); + visitor->Trace(outstanding_request_queries_); + visitor->Trace(fullscreen_event_manager_); + visitor->Trace(fullscreen_exit_observer_); + ExecutionContextLifecycleObserver::Trace(visitor); + EventTargetWithInlineData::Trace(visitor); +} + +} // namespace blink |