blob: 287e88c0c1aa016691909a3779f26b0e8b06fce9 [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 "services/metrics/public/cpp/ukm_builders.h"
#include "services/service_manager/public/cpp/interface_provider.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/modules/event_modules.h"
#include "third_party/blink/renderer/modules/event_target_modules.h"
#include "third_party/blink/renderer/modules/xr/xr_device.h"
#include "third_party/blink/renderer/platform/feature_policy/feature_policy.h"
namespace blink {
namespace {
const char kNavigatorDetachedError[] =
"The navigator.xr object is no longer associated with a document.";
const char kFeaturePolicyBlocked[] =
"Access to the feature \"xr\" is disallowed by feature policy.";
const char kNoDevicesMessage[] = "No devices found.";
} // namespace
XR::XR(LocalFrame& frame, int64_t ukm_source_id)
: ContextLifecycleObserver(frame.GetDocument()),
FocusChangedObserver(frame.GetPage()),
ukm_source_id_(ukm_source_id),
binding_(this) {
frame.GetInterfaceProvider().GetInterface(mojo::MakeRequest(&service_));
service_.set_connection_error_handler(
WTF::Bind(&XR::Dispose, WrapWeakPersistent(this)));
}
void XR::FocusedFrameChanged() {
// Tell device that focus changed.
if (device_)
device_->OnFrameFocusChanged();
}
bool XR::IsFrameFocused() {
return FocusChangedObserver::IsFrameFocused(GetFrame());
}
ExecutionContext* XR::GetExecutionContext() const {
return ContextLifecycleObserver::GetExecutionContext();
}
const AtomicString& XR::InterfaceName() const {
return EventTargetNames::XR;
}
ScriptPromise XR::requestDevice(ScriptState* script_state) {
LocalFrame* frame = GetFrame();
if (!frame) {
// Reject if the frame is inaccessible.
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kInvalidStateError,
kNavigatorDetachedError));
}
if (!did_log_requestDevice_ && frame->GetDocument()) {
ukm::builders::XR_WebXR(ukm_source_id_)
.SetDidRequestAvailableDevices(1)
.Record(frame->GetDocument()->UkmRecorder());
did_log_requestDevice_ = true;
}
if (!frame->IsFeatureEnabled(mojom::FeaturePolicyFeature::kWebVr,
ReportOptions::kReportOnFailure)) {
// Only allow the call to be made if the appropraite feature policy is in
// place.
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kSecurityError,
kFeaturePolicyBlocked));
}
// If we're still waiting for a previous call to resolve return that promise
// again.
if (pending_devices_resolver_) {
return pending_devices_resolver_->Promise();
}
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
// If we no longer have a valid service connection reject the request.
if (!service_) {
resolver->Reject(DOMException::Create(DOMExceptionCode::kNotFoundError,
kNoDevicesMessage));
return promise;
}
// If we already have a device, use that.
if (device_) {
resolver->Resolve(device_);
return promise;
}
// Otherwise wait for device request callback.
pending_devices_resolver_ = resolver;
// If we're waiting for sync, then request device is already underway.
if (pending_sync_) {
return promise;
}
service_->RequestDevice(
WTF::Bind(&XR::OnRequestDeviceReturned, WrapPersistent(this)));
pending_sync_ = true;
return promise;
}
// This will call when the XRDevice and its capabilities has potentially
// changed. For example, if a new physical device was connected to the system,
// the XRDevice might now be able to support immersive sessions, where it
// couldn't before.
void XR::OnDeviceChanged() {
DispatchEvent(*blink::Event::Create(EventTypeNames::devicechange));
}
void XR::OnRequestDeviceReturned(device::mojom::blink::XRDevicePtr device) {
pending_sync_ = false;
if (device) {
device_ = new XRDevice(this, std::move(device));
}
ResolveRequestDevice();
}
// Called when details for every connected XRDevice has been received.
void XR::ResolveRequestDevice() {
if (pending_devices_resolver_) {
if (!device_) {
pending_devices_resolver_->Reject(DOMException::Create(
DOMExceptionCode::kNotFoundError, kNoDevicesMessage));
} else {
// Log metrics
if (!did_log_returned_device_ || !did_log_supports_immersive_) {
Document* doc = GetFrame() ? GetFrame()->GetDocument() : nullptr;
if (doc) {
ukm::builders::XR_WebXR ukm_builder(ukm_source_id_);
ukm_builder.SetReturnedDevice(1);
did_log_returned_device_ = true;
ukm_builder.Record(doc->UkmRecorder());
device::mojom::blink::XRSessionOptionsPtr session_options =
device::mojom::blink::XRSessionOptions::New();
session_options->immersive = true;
// TODO(http://crbug.com/872086) This shouldn't need to be called.
// This information should be logged on the browser side.
device_->xrDevicePtr()->SupportsSession(
std::move(session_options),
WTF::Bind(&XR::ReportImmersiveSupported, WrapPersistent(this)));
}
}
// Return the device.
pending_devices_resolver_->Resolve(device_);
}
pending_devices_resolver_ = nullptr;
}
}
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 we don't have device and there is no sync pending, then request the
// device to ensure devices have been enumerated and register as a listener
// for changes.
if (event_type == EventTypeNames::devicechange && !device_ &&
!pending_sync_) {
device::mojom::blink::VRServiceClientPtr client;
binding_.Bind(mojo::MakeRequest(&client));
service_->RequestDevice(
WTF::Bind(&XR::OnRequestDeviceReturned, WrapPersistent(this)));
service_->SetClient(std::move(client));
pending_sync_ = true;
}
};
void XR::ContextDestroyed(ExecutionContext*) {
Dispose();
}
void XR::Dispose() {
// If the document context was destroyed, shut down the client connection
// and never call the mojo service again.
service_.reset();
binding_.Close();
// Shutdown device's message pipe.
if (device_)
device_->Dispose();
// Ensure that any outstanding requestDevice promises are resolved. They will
// receive a null result.
ResolveRequestDevice();
}
void XR::Trace(blink::Visitor* visitor) {
visitor->Trace(device_);
visitor->Trace(pending_devices_resolver_);
ContextLifecycleObserver::Trace(visitor);
EventTargetWithInlineData::Trace(visitor);
}
} // namespace blink