blob: fda8db7874ce4b09f8c02a6be5deba7af53b68b7 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/vr/navigator_vr.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom-blink.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/platform/web_security_origin.h"
#include "third_party/blink/public/web/web_frame.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/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/navigator.h"
#include "third_party/blink/renderer/core/frame/use_counter.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/modules/vr/vr_controller.h"
#include "third_party/blink/renderer/modules/vr/vr_display.h"
#include "third_party/blink/renderer/modules/vr/vr_get_devices_callback.h"
#include "third_party/blink/renderer/modules/vr/vr_pose.h"
#include "third_party/blink/renderer/modules/xr/xr.h"
namespace blink {
namespace {
const char kFeaturePolicyBlockedMessage[] =
"Access to the feature \"vr\" is disallowed by feature policy.";
const char kNotAssociatedWithDocumentMessage[] =
"The object is no longer associated with a document.";
const char kCannotUseBothNewAndOldAPIMessage[] =
"Cannot use navigator.getVRDisplays if the XR API is already in "
"use.";
} // namespace
NavigatorVR* NavigatorVR::From(Document& document) {
if (!document.GetFrame() || !document.GetFrame()->DomWindow())
return nullptr;
Navigator& navigator = *document.GetFrame()->DomWindow()->navigator();
return &From(navigator);
}
NavigatorVR& NavigatorVR::From(Navigator& navigator) {
NavigatorVR* supplement = Supplement<Navigator>::From<NavigatorVR>(navigator);
if (!supplement) {
supplement = MakeGarbageCollected<NavigatorVR>(navigator);
ProvideTo(navigator, supplement);
}
return *supplement;
}
XR* NavigatorVR::xr(Navigator& navigator) {
// Always return null when the navigator is detached.
if (!navigator.GetFrame())
return nullptr;
return NavigatorVR::From(navigator).xr();
}
XR* NavigatorVR::xr() {
if (!did_log_NavigatorXR_) {
ukm::builders::XR_WebXR(GetSourceId())
.SetDidUseNavigatorXR(1)
.Record(GetDocument()->UkmRecorder());
did_log_NavigatorXR_ = true;
}
LocalFrame* frame = GetSupplementable()->GetFrame();
// Always return null when the navigator is detached.
if (!frame)
return nullptr;
if (!xr_) {
// For the sake of simplicity we're going to block developers from using the
// new API if they've already made calls to the legacy API.
if (controller_) {
if (frame->GetDocument()) {
frame->GetDocument()->AddConsoleMessage(ConsoleMessage::Create(
kOtherMessageSource, kErrorMessageLevel,
"Cannot use navigator.xr if the legacy VR API is already in use."));
}
return nullptr;
}
xr_ = XR::Create(*frame, ukm_source_id_);
MaybeLogDidUseGamepad();
}
return xr_;
}
void NavigatorVR::SetDidUseGamepad() {
did_use_gamepad_ = true;
MaybeLogDidUseGamepad();
}
void NavigatorVR::MaybeLogDidUseGamepad() {
// If we have used WebXR and Gamepad, and haven't already logged the metric,
// record that Gamepad is used.
if (xr_ && did_use_gamepad_ && !did_log_did_use_gamepad_) {
ukm::builders::XR_WebXR(ukm_source_id_)
.SetDidGetGamepads(1)
.Record(GetDocument()->UkmRecorder());
did_log_did_use_gamepad_ = true;
}
}
ScriptPromise NavigatorVR::getVRDisplays(ScriptState* script_state,
Navigator& navigator) {
if (!navigator.GetFrame()) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kInvalidStateError,
kNotAssociatedWithDocumentMessage));
}
return NavigatorVR::From(navigator).getVRDisplays(script_state);
}
ScriptPromise NavigatorVR::getVRDisplays(ScriptState* script_state) {
if (!GetDocument()) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kInvalidStateError,
kNotAssociatedWithDocumentMessage));
}
if (!did_log_getVRDisplays_ && GetDocument()->IsInMainFrame()) {
did_log_getVRDisplays_ = true;
ukm::builders::XR_WebXR(GetDocument()->UkmSourceID())
.SetDidRequestAvailableDevices(1)
.Record(GetDocument()->UkmRecorder());
}
LocalFrame* frame = GetDocument()->GetFrame();
if (!frame) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kInvalidStateError,
kNotAssociatedWithDocumentMessage));
}
if (!GetDocument()->IsFeatureEnabled(mojom::FeaturePolicyFeature::kWebVr,
ReportOptions::kReportOnFailure)) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kSecurityError,
kFeaturePolicyBlockedMessage));
}
// Similar to the restriciton above, we're going to block developers from
// using the legacy API if they've already made calls to the new API.
if (xr_) {
return ScriptPromise::RejectWithDOMException(
script_state, DOMException::Create(DOMExceptionCode::kInvalidStateError,
kCannotUseBothNewAndOldAPIMessage));
}
UseCounter::Count(*GetDocument(), WebFeature::kVRGetDisplays);
ExecutionContext* execution_context = ExecutionContext::From(script_state);
if (!execution_context->IsSecureContext())
UseCounter::Count(*GetDocument(), WebFeature::kVRGetDisplaysInsecureOrigin);
Platform::Current()->RecordRapporURL("VR.WebVR.GetDisplays",
GetDocument()->Url());
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
Controller()->GetDisplays(resolver);
return promise;
}
VRController* NavigatorVR::Controller() {
if (!GetSupplementable()->GetFrame())
return nullptr;
if (!controller_) {
controller_ = MakeGarbageCollected<VRController>(this);
controller_->SetListeningForActivate(focused_ && listening_for_activate_);
controller_->FocusChanged();
}
return controller_;
}
Document* NavigatorVR::GetDocument() {
if (!GetSupplementable() || !GetSupplementable()->GetFrame())
return nullptr;
return GetSupplementable()->GetFrame()->GetDocument();
}
void NavigatorVR::Trace(blink::Visitor* visitor) {
visitor->Trace(xr_);
visitor->Trace(controller_);
Supplement<Navigator>::Trace(visitor);
}
NavigatorVR::NavigatorVR(Navigator& navigator)
: Supplement<Navigator>(navigator),
FocusChangedObserver(navigator.GetFrame()->GetPage()),
ukm_source_id_(ukm::UkmRecorder::GetNewSourceID()) {
navigator.GetFrame()->DomWindow()->RegisterEventListenerObserver(this);
FocusedFrameChanged();
if (navigator.GetFrame() && WebFrame::FromFrame(navigator.GetFrame())) {
WebFrame* main_frame = WebFrame::FromFrame(navigator.GetFrame())->Top();
if (main_frame) {
url::Origin main_frame_origin = main_frame->GetSecurityOrigin();
GetDocument()->UkmRecorder()->UpdateSourceURL(ukm_source_id_,
main_frame_origin.GetURL());
}
}
}
int64_t NavigatorVR::GetSourceId() const {
return ukm_source_id_;
}
NavigatorVR::~NavigatorVR() = default;
const char NavigatorVR::kSupplementName[] = "NavigatorVR";
void NavigatorVR::EnqueueVREvent(VRDisplayEvent* event) {
if (!GetSupplementable()->GetFrame())
return;
GetSupplementable()->GetFrame()->DomWindow()->EnqueueWindowEvent(
*event, TaskType::kMiscPlatformAPI);
}
void NavigatorVR::DispatchVREvent(VRDisplayEvent* event) {
if (!(GetSupplementable()->GetFrame()))
return;
LocalDOMWindow* window = GetSupplementable()->GetFrame()->DomWindow();
DCHECK(window);
event->SetTarget(window);
window->DispatchEvent(*event);
}
void NavigatorVR::FocusedFrameChanged() {
bool focused = IsFrameFocused(GetSupplementable()->GetFrame());
if (focused == focused_)
return;
focused_ = focused;
if (controller_) {
controller_->SetListeningForActivate(listening_for_activate_ && focused);
controller_->FocusChanged();
}
}
void NavigatorVR::DidAddEventListener(LocalDOMWindow* window,
const AtomicString& event_type) {
// Don't bother if we're using the newer API
if (xr_)
return;
if (event_type == event_type_names::kVrdisplayactivate) {
listening_for_activate_ = true;
Controller()->SetListeningForActivate(focused_);
} else if (event_type == event_type_names::kVrdisplayconnect) {
// If the page is listening for connection events make sure we've created a
// controller so that we'll be notified of new devices.
Controller();
}
}
void NavigatorVR::DidRemoveEventListener(LocalDOMWindow* window,
const AtomicString& event_type) {
// Don't bother if we're using the newer API
if (xr_)
return;
if (event_type == event_type_names::kVrdisplayactivate &&
!window->HasEventListeners(event_type_names::kVrdisplayactivate)) {
listening_for_activate_ = false;
Controller()->SetListeningForActivate(false);
}
}
void NavigatorVR::DidRemoveAllEventListeners(LocalDOMWindow* window) {
if (xr_ || !controller_)
return;
controller_->SetListeningForActivate(false);
listening_for_activate_ = false;
}
} // namespace blink