blob: f869a3d48c098ea88d881020a7759967d4e4800c [file] [log] [blame]
// 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.h"
#include <utility>
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom-blink.h"
#include "third_party/blink/public/platform/interface_provider.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.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/frame/local_frame.h"
#include "third_party/blink/renderer/core/inspector/console_message.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 'XR': The provided value 'immersive-ar' is not "
"a valid enum value of type XRSessionMode.";
// Helper method to convert session mode into Mojo options.
device::mojom::blink::XRSessionOptionsPtr convertModeToMojo(
XRSession::SessionMode mode) {
auto session_options = device::mojom::blink::XRSessionOptions::New();
session_options->immersive = (mode == XRSession::kModeImmersiveVR ||
mode == XRSession::kModeImmersiveAR);
session_options->environment_integration =
mode == XRSession::kModeImmersiveAR;
return session_options;
}
XRSession::SessionMode stringToSessionMode(const String& mode_string) {
if (mode_string == "inline") {
return XRSession::kModeInline;
}
if (mode_string == "immersive-vr") {
return XRSession::kModeImmersiveVR;
}
if (mode_string == "immersive-ar") {
return XRSession::kModeImmersiveAR;
}
NOTREACHED(); // Only strings in the enum are allowed by IDL.
return XRSession::kModeInline;
}
const char* SessionModeToString(XRSession::SessionMode session_mode) {
switch (session_mode) {
case XRSession::SessionMode::kModeInline:
return "inline";
case XRSession::SessionMode::kModeImmersiveVR:
return "immersive-vr";
case XRSession::SessionMode::kModeImmersiveAR:
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 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;
}
return base::nullopt;
}
bool IsFeatureValidForMode(device::mojom::XRSessionFeature feature,
XRSession::SessionMode mode) {
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:
return mode == XRSession::kModeImmersiveVR ||
mode == XRSession::kModeImmersiveAR;
}
}
template <typename Fn>
XRSessionFeatureSet ParseRequestedFeatures(
const HeapVector<ScriptValue>& features,
XRSession::SessionMode session_mode,
Fn&& error_fn) {
XRSessionFeatureSet 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(feature_string);
if (!feature_enum) {
String error_message =
"Unrecognized feature requested: " + feature_string;
error_fn(std::move(error_message));
} else if (!IsFeatureValidForMode(feature_enum.value(), session_mode)) {
String error_message =
"Feature '" + feature_string +
"' is not supported for mode: " + SessionModeToString(session_mode);
error_fn(std::move(error_message));
} else {
result.insert(feature_enum.value());
}
} else {
error_fn("Unrecognized feature value");
}
}
return result;
}
// 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;
}
if (!doc->IsFeatureEnabled(mojom::FeaturePolicyFeature::kWebXr,
ReportOptions::kReportOnFailure)) {
return kFeaturePolicyBlocked;
}
// 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* XR::CheckInlineSessionRequestAllowed(
LocalFrame* frame,
Document* doc,
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;
}
}
}
// Make sure the WebXR feature policy is enabled
if (!doc->IsFeatureEnabled(mojom::FeaturePolicyFeature::kWebXr,
ReportOptions::kReportOnFailure)) {
return kFeaturePolicyBlocked;
}
return nullptr;
}
XR::PendingSupportsSessionQuery::PendingSupportsSessionQuery(
ScriptPromiseResolver* resolver,
XRSession::SessionMode session_mode)
: resolver_(resolver), mode_(session_mode) {}
void XR::PendingSupportsSessionQuery::Trace(blink::Visitor* visitor) {
visitor->Trace(resolver_);
}
void XR::PendingSupportsSessionQuery::Resolve() {
resolver_->Resolve();
}
void XR::PendingSupportsSessionQuery::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);
} else {
resolver_->Reject(
MakeGarbageCollected<DOMException>(exception_code, message));
}
}
void XR::PendingSupportsSessionQuery::RejectWithSecurityError(
const String& sanitized_message,
ExceptionState* exception_state) {
if (exception_state) {
exception_state->ThrowSecurityError(sanitized_message);
} else {
resolver_->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError, sanitized_message));
}
}
void XR::PendingSupportsSessionQuery::RejectWithTypeError(
const String& message,
ExceptionState* exception_state) {
if (exception_state) {
exception_state->ThrowTypeError(message);
} else {
resolver_->Reject(V8ThrowException::CreateTypeError(
resolver_->GetScriptState()->GetIsolate(), message));
}
}
XRSession::SessionMode XR::PendingSupportsSessionQuery::mode() const {
return mode_;
}
XR::PendingRequestSessionQuery::PendingRequestSessionQuery(
int64_t ukm_source_id,
ScriptPromiseResolver* resolver,
XRSession::SessionMode 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) {}
void XR::PendingRequestSessionQuery::Resolve(XRSession* session) {
resolver_->Resolve(session);
ReportRequestSessionResult(SessionRequestStatus::kSuccess);
}
void XR::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);
} else {
resolver_->Reject(
MakeGarbageCollected<DOMException>(exception_code, message));
}
ReportRequestSessionResult(SessionRequestStatus::kOtherError);
}
void XR::PendingRequestSessionQuery::RejectWithSecurityError(
const String& sanitized_message,
ExceptionState* exception_state) {
if (exception_state) {
exception_state->ThrowSecurityError(sanitized_message);
} else {
resolver_->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kSecurityError, sanitized_message));
}
ReportRequestSessionResult(SessionRequestStatus::kOtherError);
}
void XR::PendingRequestSessionQuery::RejectWithTypeError(
const String& message,
ExceptionState* exception_state) {
if (exception_state) {
exception_state->ThrowTypeError(message);
} else {
resolver_->Reject(V8ThrowException::CreateTypeError(
GetScriptState()->GetIsolate(), message));
}
ReportRequestSessionResult(SessionRequestStatus::kOtherError);
}
void XR::PendingRequestSessionQuery::ReportRequestSessionResult(
SessionRequestStatus status) {
LocalFrame* frame = resolver_->GetFrame();
Document* doc = frame ? frame->GetDocument() : nullptr;
if (!doc)
return;
ukm::builders::XR_WebXR_SessionRequest(ukm_source_id_)
.SetMode(static_cast<int64_t>(mode_))
.SetStatus(static_cast<int64_t>(status))
.Record(doc->UkmRecorder());
}
XRSession::SessionMode XR::PendingRequestSessionQuery::mode() const {
return mode_;
}
const XRSessionFeatureSet& XR::PendingRequestSessionQuery::RequiredFeatures()
const {
return required_features_.valid_features;
}
const XRSessionFeatureSet& XR::PendingRequestSessionQuery::OptionalFeatures()
const {
return optional_features_.valid_features;
}
bool XR::PendingRequestSessionQuery::InvalidRequiredFeatures() const {
return required_features_.invalid_features;
}
bool XR::PendingRequestSessionQuery::InvalidOptionalFeatures() const {
return optional_features_.invalid_features;
}
ScriptState* XR::PendingRequestSessionQuery::GetScriptState() const {
return resolver_->GetScriptState();
}
void XR::PendingRequestSessionQuery::Trace(blink::Visitor* visitor) {
visitor->Trace(resolver_);
}
device::mojom::blink::XRSessionOptionsPtr XR::XRSessionOptionsFromQuery(
const PendingRequestSessionQuery& query) {
device::mojom::blink::XRSessionOptionsPtr session_options =
convertModeToMojo(query.mode());
CopyToVector(query.RequiredFeatures(), session_options->required_features);
CopyToVector(query.OptionalFeatures(), session_options->optional_features);
return session_options;
}
XR::XR(LocalFrame& frame, int64_t ukm_source_id)
: ContextLifecycleObserver(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.GetInterfaceProvider().GetInterface(mojo::MakeRequest(
&service_, frame.GetTaskRunner(TaskType::kMiscPlatformAPI)));
service_.set_connection_error_handler(
WTF::Bind(&XR::Dispose, WrapWeakPersistent(this)));
}
void XR::FocusedFrameChanged() {
// Tell all sessions that focus changed.
for (const auto& session : sessions_) {
session->OnFocusChanged();
}
if (frame_provider_)
frame_provider_->OnFocusChanged();
}
bool XR::IsFrameFocused() {
return FocusChangedObserver::IsFrameFocused(GetFrame());
}
ExecutionContext* XR::GetExecutionContext() const {
return ContextLifecycleObserver::GetExecutionContext();
}
const AtomicString& XR::InterfaceName() const {
return event_target_names::kXR;
}
XRFrameProvider* XR::frameProvider() {
if (!frame_provider_) {
frame_provider_ = MakeGarbageCollected<XRFrameProvider>(this);
}
return frame_provider_;
}
bool XR::CanRequestNonImmersiveFrameData() const {
return !!magic_window_provider_;
}
void XR::GetNonImmersiveFrameData(
device::mojom::blink::XRFrameDataRequestOptionsPtr options,
device::mojom::blink::XRFrameDataProvider::GetFrameDataCallback callback) {
DCHECK(CanRequestNonImmersiveFrameData());
magic_window_provider_->GetFrameData(std::move(options), std::move(callback));
}
const device::mojom::blink::XREnvironmentIntegrationProviderAssociatedPtr&
XR::xrEnvironmentProviderPtr() {
return environment_provider_;
}
void XR::AddEnvironmentProviderErrorHandler(
EnvironmentProviderErrorCallback callback) {
environment_provider_error_callbacks_.push_back(std::move(callback));
}
void XR::ExitPresent() {
DCHECK(service_);
service_->ExitPresent();
}
ScriptPromise XR::supportsSession(ScriptState* script_state,
const String& mode,
ExceptionState& exception_state) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
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 promise;
}
XRSession::SessionMode session_mode = stringToSessionMode(mode);
PendingSupportsSessionQuery* query =
MakeGarbageCollected<PendingSupportsSessionQuery>(resolver, session_mode);
if (session_mode == XRSession::kModeImmersiveAR &&
!RuntimeEnabledFeatures::WebXRARModuleEnabled(doc)) {
query->RejectWithTypeError(
String::Format(kImmersiveArModeNotValid, "supportsSession"),
&exception_state);
return promise;
}
if (!doc->IsFeatureEnabled(mojom::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 (session_mode == XRSession::kModeInline) {
// `inline` sessions are always supported if not blocked by feature policy.
query->Resolve();
} else {
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->RejectWithDOMException(DOMExceptionCode::kNotSupportedError,
kSessionNotSupported, &exception_state);
return promise;
}
device::mojom::blink::XRSessionOptionsPtr session_options =
convertModeToMojo(query->mode());
outstanding_support_queries_.insert(query);
service_->SupportsSession(
std::move(session_options),
WTF::Bind(&XR::OnSupportsSessionReturned, WrapPersistent(this),
WrapPersistent(query)));
}
return promise;
}
void XR::RequestImmersiveSession(LocalFrame* frame,
Document* doc,
PendingRequestSessionQuery* query,
ExceptionState* exception_state) {
// 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;
}
// If its an immersive AR session, make sure that feature is enabled
if (query->mode() == XRSession::kModeImmersiveAR &&
!RuntimeEnabledFeatures::WebXRARModuleEnabled(doc)) {
query->RejectWithTypeError(
String::Format(kImmersiveArModeNotValid, "requestSession"),
exception_state);
return;
}
// 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()) {
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);
service_->RequestSession(
std::move(session_options),
WTF::Bind(&XR::OnRequestSessionReturned, WrapWeakPersistent(this),
WrapPersistent(query)));
}
void XR::RequestInlineSession(LocalFrame* frame,
Document* doc,
PendingRequestSessionQuery* query,
ExceptionState* exception_state) {
// Make sure the inline session request was allowed
auto* inline_session_request_error =
CheckInlineSessionRequestAllowed(frame, doc, *query);
if (inline_session_request_error) {
query->RejectWithSecurityError(inline_session_request_error,
exception_state);
return;
}
if (!service_) {
// If we don't have a service by the time we reach this call, there is no XR
// hardware. Create a sensorless session if possible.
if (CanCreateSensorlessInlineSession(query)) {
XRSession* session = CreateSensorlessInlineSession();
query->Resolve(session);
} else {
query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError,
kSessionNotSupported, exception_state);
}
return;
}
// Reject session if any of the required features were invalid.
if (query->InvalidRequiredFeatures()) {
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(&XR::OnRequestSessionReturned, WrapWeakPersistent(this),
WrapPersistent(query)));
}
ScriptPromise XR::requestSession(ScriptState* script_state,
const String& mode,
XRSessionInit* session_init,
ExceptionState& exception_state) {
// TODO(https://crbug.com/968622): Make sure we don't forget to call
// metrics-related methods when the promise gets resolved/rejected.
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
ScriptPromise promise = resolver->Promise();
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 promise;
}
XRSession::SessionMode session_mode = stringToSessionMode(mode);
// Parse required feature strings
RequestedXRSessionFeatureSet required_features;
if (session_init && session_init->hasRequiredFeatures()) {
required_features.valid_features = ParseRequestedFeatures(
session_init->requiredFeatures(), session_mode,
[&](const String& error) {
GetExecutionContext()->AddConsoleMessage(ConsoleMessage::Create(
mojom::ConsoleMessageSource::kJavaScript,
mojom::ConsoleMessageLevel::kError, error));
required_features.invalid_features = true;
});
}
// Parse optional feature strings
RequestedXRSessionFeatureSet optional_features;
if (session_init && session_init->hasOptionalFeatures()) {
optional_features.valid_features = ParseRequestedFeatures(
session_init->optionalFeatures(), session_mode,
[&](const String& error) {
GetExecutionContext()->AddConsoleMessage(ConsoleMessage::Create(
mojom::ConsoleMessageSource::kJavaScript,
mojom::ConsoleMessageLevel::kWarning, error));
optional_features.invalid_features = true;
});
}
// Certain session modes imply default features.
// Add those default features as optional features now.
switch (session_mode) {
case XRSession::kModeImmersiveVR:
case XRSession::kModeImmersiveAR:
optional_features.valid_features.insert(
device::mojom::XRSessionFeature::REF_SPACE_LOCAL);
FALLTHROUGH;
case XRSession::kModeInline:
optional_features.valid_features.insert(
device::mojom::XRSessionFeature::REF_SPACE_VIEWER);
break;
}
PendingRequestSessionQuery* query =
MakeGarbageCollected<PendingRequestSessionQuery>(
GetSourceId(), resolver, session_mode, std::move(required_features),
std::move(optional_features));
switch (session_mode) {
case XRSession::kModeImmersiveVR:
case XRSession::kModeImmersiveAR:
RequestImmersiveSession(frame, doc, query, &exception_state);
break;
case XRSession::kModeInline:
RequestInlineSession(frame, doc, 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 XR::OnDeviceChanged() {
DispatchEvent(*blink::Event::Create(event_type_names::kDevicechange));
}
void XR::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);
if (supports_session) {
query->Resolve();
} else {
query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError,
kSessionNotSupported, nullptr);
}
}
void XR::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() == XRSession::kModeImmersiveVR ||
query->mode() == XRSession::kModeImmersiveAR) {
DCHECK(has_outstanding_immersive_request_);
has_outstanding_immersive_request_ = false;
}
device::mojom::blink::XRSessionPtr session_ptr =
result->is_session() ? std::move(result->get_session()) : nullptr;
// TODO(https://crbug.com/872316) Improve the error messaging to indicate why
// a request failed.
if (!session_ptr) {
// |service_| does not support the requested mode. Attempt to create a
// sensorless session.
if (CanCreateSensorlessInlineSession(query)) {
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;
}
bool environment_integration = query->mode() == XRSession::kModeImmersiveAR;
// immersive sessions must supply display info.
DCHECK(session_ptr->display_info);
// If the session supports environment integration, ensure the device does
// as well.
DCHECK(!environment_integration || session_ptr->display_info->capabilities
->can_provide_environment_integration);
DVLOG(2) << __func__
<< ": environment_integration=" << environment_integration
<< "can_provide_environment_integration="
<< session_ptr->display_info->capabilities
->can_provide_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_request),
std::move(session_ptr->display_info), session_ptr->uses_input_eventing,
enabled_features);
if (query->mode() == XRSession::kModeImmersiveVR ||
query->mode() == XRSession::kModeImmersiveAR) {
frameProvider()->BeginImmersiveSession(session, std::move(session_ptr));
if (environment_integration) {
// See Task Sources spreadsheet for more information:
// https://docs.google.com/spreadsheets/d/1b-dus1Ug3A8y0lX0blkmOjJILisUASdj8x9YN_XMwYc/view
frameProvider()->GetDataProvider()->GetEnvironmentIntegrationProvider(
mojo::MakeRequest(&environment_provider_,
GetExecutionContext()->GetTaskRunner(
TaskType::kMiscPlatformAPI)));
environment_provider_.set_connection_error_handler(WTF::Bind(
&XR::OnEnvironmentProviderDisconnect, WrapWeakPersistent(this)));
}
if (query->mode() == XRSession::kModeImmersiveVR &&
session->UsesInputEventing()) {
frameProvider()->GetDataProvider()->SetInputSourceButtonListener(
session->GetInputClickListener());
}
} else {
magic_window_provider_.Bind(std::move(session_ptr->data_provider));
magic_window_provider_.set_connection_error_handler(WTF::Bind(
&XR::OnMagicWindowProviderDisconnect, WrapWeakPersistent(this)));
}
UseCounter::Count(ExecutionContext::From(query->GetScriptState()),
WebFeature::kWebXrSessionCreated);
query->Resolve(session);
}
void XR::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 XR::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 XR::ContextDestroyed(ExecutionContext*) {
Dispose();
}
// A session is always created and returned.
XRSession* XR::CreateSession(
XRSession::SessionMode mode,
XRSession::EnvironmentBlendMode blend_mode,
device::mojom::blink::XRSessionClientRequest client_request,
device::mojom::blink::VRDisplayInfoPtr display_info,
bool uses_input_eventing,
XRSessionFeatureSet enabled_features,
bool sensorless_session) {
XRSession* session = MakeGarbageCollected<XRSession>(
this, client_request ? std::move(client_request) : nullptr, 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;
}
bool XR::CanCreateSensorlessInlineSession(
const PendingRequestSessionQuery* query) const {
// Sensorless can only support an inline mode
if (query->mode() != XRSession::kModeInline)
return false;
// Sensorless can only be supported if the only required feature is the
// viewer reference space.
for (const auto& feature : query->RequiredFeatures()) {
if (feature != device::mojom::XRSessionFeature::REF_SPACE_VIEWER) {
return false;
}
}
return true;
}
XRSession* XR::CreateSensorlessInlineSession() {
// TODO(https://crbug.com/944936): The blend mode could be "additive".
XRSession::EnvironmentBlendMode blend_mode = XRSession::kBlendModeOpaque;
return CreateSession(XRSession::kModeInline, blend_mode,
nullptr /* client request */, nullptr /* display_info */,
false /* uses_input_eventing */,
{device::mojom::XRSessionFeature::REF_SPACE_VIEWER},
true /* sensorless_session */);
}
void XR::Dispose() {
// 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 XR::OnEnvironmentProviderDisconnect() {
for (auto& callback : environment_provider_error_callbacks_) {
std::move(callback).Run();
}
environment_provider_error_callbacks_.clear();
environment_provider_.reset();
}
// Ends all non-immersive sessions when the magic window provider got
// disconnected.
void XR::OnMagicWindowProviderDisconnect() {
for (auto& session : sessions_) {
if (!session->immersive() && !session->ended()) {
session->ForceEnd();
}
}
magic_window_provider_.reset();
}
void XR::Trace(blink::Visitor* visitor) {
visitor->Trace(frame_provider_);
visitor->Trace(sessions_);
visitor->Trace(outstanding_support_queries_);
visitor->Trace(outstanding_request_queries_);
ContextLifecycleObserver::Trace(visitor);
EventTargetWithInlineData::Trace(visitor);
}
} // namespace blink