// Copyright 2017 The Chromium Authors
// 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 <algorithm>
#include <memory>
#include <utility>

#include "base/feature_list.h"
#include "base/trace_event/trace_id_helper.h"
#include "base/trace_event/typed_macros.h"
#include "build/build_config.h"
#include "device/vr/public/mojom/vr_service.mojom-blink.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-blink.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_xr_depth_state_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_xr_reference_space_type.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_xr_session_mode.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_xr_tracked_image_init.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_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/settings.h"
#include "third_party/blink/renderer/core/frame/viewport_data.h"
#include "third_party/blink/renderer/core/fullscreen/fullscreen.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/imagebitmap/image_bitmap.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_enter_fullscreen_observer.h"
#include "third_party/blink/renderer/modules/xr/xr_exit_fullscreen_observer.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/modules/xr/xr_session_viewport_scaler.h"
#include "third_party/blink/renderer/modules/xr/xr_utils.h"
#include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h"
#include "third_party/blink/renderer/platform/graphics/static_bitmap_image.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.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 {

constexpr uint64_t kInvalidTraceId = -1;

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 permissions 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 kInvalidRequiredFeatures[] =
    "The session request contains invalid requiredFeatures and could not be "
    "fullfilled.";

const char kNoDevicesMessage[] = "No XR hardware found.";

const char kImmersiveArModeNotValid[] =
    "Failed to execute 'requestSession' 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 V8EnumToSessionMode(
    V8XRSessionMode::Enum mode) {
  switch (mode) {
    case V8XRSessionMode::Enum::kInline:
      return device::mojom::blink::XRSessionMode::kInline;
    case V8XRSessionMode::Enum::kImmersiveVr:
      return device::mojom::blink::XRSessionMode::kImmersiveVr;
    case V8XRSessionMode::Enum::kImmersiveAr:
      return device::mojom::blink::XRSessionMode::kImmersiveAr;
  }
}

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";
  }
}

device::mojom::XRDepthUsage ParseDepthUsage(const V8XRDepthUsage& usage) {
  switch (usage.AsEnum()) {
    case V8XRDepthUsage::Enum::kCpuOptimized:
      return device::mojom::XRDepthUsage::kCPUOptimized;
    case V8XRDepthUsage::Enum::kGpuOptimized:
      return device::mojom::XRDepthUsage::kGPUOptimized;
  }
}

device::mojom::XRDepthDataFormat ParseDepthFormat(
    const V8XRDepthDataFormat& format) {
  switch (format.AsEnum()) {
    case V8XRDepthDataFormat::Enum::kLuminanceAlpha:
      return device::mojom::XRDepthDataFormat::kLuminanceAlpha;
    case V8XRDepthDataFormat::Enum::kFloat32:
      return device::mojom::XRDepthDataFormat::kFloat32;
    case V8XRDepthDataFormat::Enum::kUnsignedShort:
      return device::mojom::XRDepthDataFormat::kUnsignedShort;
  }
}

device::mojom::XRDepthType ParseDepthType(const V8XRDepthType& type) {
  switch (type.AsEnum()) {
    case V8XRDepthType::Enum::kRaw:
      return device::mojom::XRDepthType::kRaw;
    case V8XRDepthType::Enum::kSmooth:
      return device::mojom::XRDepthType::kSmooth;
  }
}

bool IsFeatureValidForMode(device::mojom::XRSessionFeature feature,
                           device::mojom::blink::XRSessionMode mode,
                           XRSessionInit* session_init,
                           ExecutionContext* execution_context,
                           mojom::blink::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:
    case device::mojom::XRSessionFeature::ANCHORS:
    case device::mojom::XRSessionFeature::HAND_INPUT:
    case device::mojom::XRSessionFeature::SECONDARY_VIEWS:
    case device::mojom::XRSessionFeature::LAYERS:
    case device::mojom::XRSessionFeature::WEBGPU:
      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::blink::ConsoleMessageSource::kJavaScript, error_level,
            "Must specify a valid domOverlay.root element in XRSessionInit"));
        return false;
      }
      return true;
    case device::mojom::XRSessionFeature::IMAGE_TRACKING:
      if (mode != device::mojom::blink::XRSessionMode::kImmersiveAr)
        return false;
      if (!session_init->hasTrackedImages()) {
        execution_context->AddConsoleMessage(
            MakeGarbageCollected<ConsoleMessage>(
                mojom::blink::ConsoleMessageSource::kJavaScript, error_level,
                "Must specify trackedImages in XRSessionInit"));
        return false;
      }
      return true;
    case device::mojom::XRSessionFeature::LIGHT_ESTIMATION:
    case device::mojom::XRSessionFeature::CAMERA_ACCESS:
    case device::mojom::XRSessionFeature::PLANE_DETECTION:
    case device::mojom::XRSessionFeature::FRONT_FACING:
    case device::mojom::XRSessionFeature::MESH_DETECTION:
      return mode == device::mojom::blink::XRSessionMode::kImmersiveAr;
    case device::mojom::XRSessionFeature::DEPTH:
      if (!session_init->hasDepthSensing()) {
        execution_context->AddConsoleMessage(
            MakeGarbageCollected<ConsoleMessage>(
                mojom::blink::ConsoleMessageSource::kJavaScript, error_level,
                "Must provide a depthSensing dictionary in XRSessionInit"));
        return false;
      }
      return mode == device::mojom::blink::XRSessionMode::kImmersiveAr;
  }
}

bool HasRequiredPermissionsPolicy(ExecutionContext* context,
                                  device::mojom::XRSessionFeature feature) {
  if (!context)
    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:
    case device::mojom::XRSessionFeature::LIGHT_ESTIMATION:
    case device::mojom::XRSessionFeature::ANCHORS:
    case device::mojom::XRSessionFeature::PLANE_DETECTION:
    case device::mojom::XRSessionFeature::MESH_DETECTION:
    case device::mojom::XRSessionFeature::DEPTH:
    case device::mojom::XRSessionFeature::IMAGE_TRACKING:
    case device::mojom::XRSessionFeature::HAND_INPUT:
    case device::mojom::XRSessionFeature::SECONDARY_VIEWS:
    case device::mojom::XRSessionFeature::LAYERS:
    case device::mojom::XRSessionFeature::FRONT_FACING:
    case device::mojom::XRSessionFeature::WEBGPU:
      return context->IsFeatureEnabled(
          network::mojom::PermissionsPolicyFeature::kWebXr,
          ReportOptions::kReportOnFailure);
    case device::mojom::XRSessionFeature::CAMERA_ACCESS:
      return context->IsFeatureEnabled(
                 network::mojom::PermissionsPolicyFeature::kWebXr,
                 ReportOptions::kReportOnFailure) &&
             context->IsFeatureEnabled(
                 network::mojom::PermissionsPolicyFeature::kCamera,
                 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(LocalDOMWindow* window) {
  // Ensure that the session was initiated by a user gesture
  if (!LocalFrame::HasTransientUserActivation(window->GetFrame())) {
    return kRequestRequiresUserActivation;
  }

  // Check that the document is "trustworthy"
  // https://immersive-web.github.io/webxr/#trustworthy
  if (!window->document()->IsPageVisible()) {
    return kPageNotVisible;
  }

  // Consent occurs in the Browser process.

  return nullptr;
}

// Helper method to convert the mojom error code into text for displaying in the
// console. The console message will have the format of:
// "Could not create a session because: <this value>"
const char* GetConsoleMessage(device::mojom::RequestSessionError error) {
  switch (error) {
    case device::mojom::RequestSessionError::EXISTING_IMMERSIVE_SESSION:
      return "There is already an existing immersive session";
    case device::mojom::RequestSessionError::INVALID_CLIENT:
      return "An error occurred while querying for runtime support";
    case device::mojom::RequestSessionError::USER_DENIED_CONSENT:
      return "The user denied some part of the requested configuration";
    case device::mojom::RequestSessionError::NO_RUNTIME_FOUND:
      return "No runtimes supported the requested configuration";
    case device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR:
      return "Something went wrong initializing the session in the runtime";
    case device::mojom::RequestSessionError::RUNTIME_INSTALL_FAILURE:
      return "The runtime for this configuration could not be installed";
    case device::mojom::RequestSessionError::RUNTIMES_CHANGED:
      return "The supported runtimes changed while initializing the session";
    case device::mojom::RequestSessionError::FULLSCREEN_ERROR:
      return "An error occurred while initializing fullscreen support";
    case device::mojom::RequestSessionError::UNKNOWN_FAILURE:
      return "An unknown error occurred";
  }
}

bool IsFeatureRequested(
    device::mojom::XRSessionFeatureRequestStatus requestStatus) {
  switch (requestStatus) {
    case device::mojom::XRSessionFeatureRequestStatus::kOptionalAccepted:
    case device::mojom::XRSessionFeatureRequestStatus::kRequired:
      return true;
    case device::mojom::XRSessionFeatureRequestStatus::kNotRequested:
    case device::mojom::XRSessionFeatureRequestStatus::kOptionalRejected:
      return false;
  }
}

bool IsImmersiveArAllowedBySettings(LocalDOMWindow* window) {
  // If we're unable to get the settings for any reason, we'll treat the AR as
  // enabled.
  if (!window->GetFrame()) {
    return true;
  }

  return window->GetFrame()->GetSettings()->GetWebXRImmersiveArAllowed();
}

// When enabled, accessing the navigator.xr attribute does not prevent the
// frame from entering the back forward cache.
// Kill switch for https://crbug.com/392087591
BASE_FEATURE(kWebXrAttributeAllowsBackForwardCache,
             base::FEATURE_ENABLED_BY_DEFAULT);

}  // 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(
    ScriptPromiseResolverBase* resolver,
    device::mojom::blink::XRSessionMode session_mode)
    : resolver_(resolver),
      mode_(session_mode),
      trace_id_(base::trace_event::GetNextGlobalTraceId()) {
  TRACE_EVENT("xr", "PendingSupportsSessionQuery::PendingSupportsSessionQuery",
              "session_mode", session_mode, perfetto::Flow::Global(trace_id_));
}

void XRSystem::PendingSupportsSessionQuery::Trace(Visitor* visitor) const {
  visitor->Trace(resolver_);
}

void XRSystem::PendingSupportsSessionQuery::Resolve(
    bool supported,
    ExceptionState* exception_state) {
  TRACE_EVENT("xr", "PendingSupportsSessionQuery::Resolve", "supported",
              supported, perfetto::TerminatingFlow::Global(trace_id_));

  static_cast<ScriptPromiseResolver<IDLBoolean>*>(resolver_.Get())
      ->Resolve(supported);
}

void XRSystem::PendingSupportsSessionQuery::RejectWithDOMException(
    DOMExceptionCode exception_code,
    const String& message,
    ExceptionState* exception_state) {
  DCHECK_NE(exception_code, DOMExceptionCode::kSecurityError);

  TRACE_EVENT("xr", "PendingSupportsSessionQuery::RejectWithDOMException",
              "exception_code", exception_code, "message", message,
              perfetto::TerminatingFlow::Global(trace_id_));

  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& message,
    ExceptionState* exception_state) {
  TRACE_EVENT("xr", "PendingSupportsSessionQuery::RejectWithSecurityError",
              "message", message, perfetto::TerminatingFlow::Global(trace_id_));

  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(message);
    resolver_->Detach();
  } else {
    resolver_->Reject(MakeGarbageCollected<DOMException>(
        DOMExceptionCode::kSecurityError, message));
  }
}

void XRSystem::PendingSupportsSessionQuery::RejectWithTypeError(
    const String& message,
    ExceptionState* exception_state) {
  TRACE_EVENT("xr", "PendingSupportsSessionQuery::RejectWithTypeError",
              "message", message, perfetto::TerminatingFlow::Global(trace_id_));

  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<XRSession>* 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),
      trace_id_(base::trace_event::GetNextGlobalTraceId()) {
  TRACE_EVENT("xr", "PendingRequestSessionQuery::PendingRequestSessionQuery",
              "Session mode", session_mode, perfetto::Flow::Global(trace_id_));

  ParseSensorRequirement();
}

void XRSystem::PendingRequestSessionQuery::Resolve(
    XRSession* session,
    mojo::PendingRemote<device::mojom::blink::XRSessionMetricsRecorder>
        metrics_recorder) {
  TRACE_EVENT("xr", "PendingRequestSessionQuery::Resolve",
              perfetto::TerminatingFlow::Global(trace_id_));

  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);

  TRACE_EVENT("xr", "PendingRequestSessionQuery::RejectWithDOMException",
              "exception_code", exception_code, "message", message,
              perfetto::TerminatingFlow::Global(trace_id_));

  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& message,
    ExceptionState* exception_state) {
  TRACE_EVENT("xr", "PendingRequestSessionQuery::RejectWithSecurityError",
              "message", message, perfetto::TerminatingFlow::Global(trace_id_));

  if (exception_state) {
    exception_state->ThrowSecurityError(message);
    resolver_->Detach();
  } else {
    resolver_->Reject(MakeGarbageCollected<DOMException>(
        DOMExceptionCode::kSecurityError, message));
  }

  ReportRequestSessionResult(SessionRequestStatus::kOtherError);
}

void XRSystem::PendingRequestSessionQuery::RejectWithTypeError(
    const String& message,
    ExceptionState* exception_state) {
  TRACE_EVENT("xr", "PendingRequestSessionQuery::RejectWithTypeError",
              "message", message, perfetto::TerminatingFlow::Global(trace_id_));

  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;
  auto* execution_context = resolver_->GetExecutionContext();
  if (!execution_context) {
    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);
  auto feature_request_depth_sensing =
      GetFeatureRequestStatus(XRSessionFeature::DEPTH, session);
  auto feature_request_plane_detection =
      GetFeatureRequestStatus(XRSessionFeature::PLANE_DETECTION, session);
  auto feature_request_image_tracking =
      GetFeatureRequestStatus(XRSessionFeature::IMAGE_TRACKING, session);
  auto feature_request_hand_input =
      GetFeatureRequestStatus(XRSessionFeature::HAND_INPUT, session);
  auto feature_request_light_estimation =
      GetFeatureRequestStatus(XRSessionFeature::LIGHT_ESTIMATION, session);
  auto feature_request_camera_access =
      GetFeatureRequestStatus(XRSessionFeature::CAMERA_ACCESS, session);
  auto feature_request_anchors =
      GetFeatureRequestStatus(XRSessionFeature::ANCHORS, session);
  auto feature_request_hit_test =
      GetFeatureRequestStatus(XRSessionFeature::HIT_TEST, session);
  auto feature_request_mesh_detection =
      GetFeatureRequestStatus(XRSessionFeature::MESH_DETECTION, 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(execution_context->UkmRecorder());

  // If the session was successfully created record use counters.
  if (session && status == SessionRequestStatus::kSuccess) {
    if (IsFeatureRequested(feature_request_dom_overlay)) {
      DVLOG(2) << __func__
               << ": DOM overlay was requested, logging a UseCounter";
      UseCounter::Count(session->GetExecutionContext(),
                        WebFeature::kXRDOMOverlay);
    }

    if (IsFeatureRequested(feature_request_depth_sensing)) {
      DVLOG(2) << __func__
               << ": depth sensing was requested, logging a UseCounter";
      UseCounter::Count(session->GetExecutionContext(),
                        WebFeature::kXRDepthSensing);
    }

    if (IsFeatureRequested(feature_request_plane_detection)) {
      DVLOG(2) << __func__
               << ": plane detection was requested, logging a UseCounter";
      UseCounter::Count(session->GetExecutionContext(),
                        WebFeature::kXRPlaneDetection);
    }

    if (IsFeatureRequested(feature_request_mesh_detection)) {
      DVLOG(2) << __func__
               << ": mesh detection was requested, logging a UseCounter";
      UseCounter::Count(session->GetExecutionContext(),
                        WebFeature::kXRMeshDetection);
    }

    if (IsFeatureRequested(feature_request_image_tracking)) {
      DVLOG(2) << __func__
               << ": image tracking was requested, logging a UseCounter";
      UseCounter::Count(session->GetExecutionContext(),
                        WebFeature::kXRImageTracking);
    }

    if (IsFeatureRequested(feature_request_hand_input)) {
      DVLOG(2) << __func__
               << ": hand input was requested, logging a UseCounter";
      UseCounter::CountWebDXFeature(session->GetExecutionContext(),
                                    WebDXFeature::kWebxrHandInput);
    }

    if (IsFeatureRequested(feature_request_light_estimation)) {
      DVLOG(2) << __func__
               << ": lighting estimation was requested, logging a UseCounter";
      UseCounter::CountWebDXFeature(session->GetExecutionContext(),
                                    WebDXFeature::kWebxrLightingEstimation);
    }

    if (IsFeatureRequested(feature_request_camera_access)) {
      DVLOG(2) << __func__
               << ": camera access was requested, logging a UseCounter";
      UseCounter::CountWebDXFeature(session->GetExecutionContext(),
                                    WebDXFeature::kWebxrCamera);
    }

    if (IsFeatureRequested(feature_request_anchors)) {
      DVLOG(2) << __func__ << ": anchors were requested, logging a UseCounter";
      UseCounter::CountWebDXFeature(session->GetExecutionContext(),
                                    WebDXFeature::kWebxrAnchors);
    }

    if (IsFeatureRequested(feature_request_hit_test)) {
      DVLOG(2) << __func__ << ": hit test was requested, logging a UseCounter";
      UseCounter::CountWebDXFeature(session->GetExecutionContext(),
                                    WebDXFeature::kWebxrHitTest);
    }
  }

  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::HasFeature(
    device::mojom::XRSessionFeature feature) const {
  return RequiredFeatures().Contains(feature) ||
         OptionalFeatures().Contains(feature);
}

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) const {
  visitor->Trace(resolver_);
  visitor->Trace(dom_overlay_element_);
}

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();

  session_options->required_features.assign(query.RequiredFeatures());
  session_options->optional_features.assign(query.OptionalFeatures());

  session_options->tracked_images.resize(query.TrackedImages().size());
  for (unsigned i = 0; i < query.TrackedImages().size(); ++i) {
    session_options->tracked_images[i] =
        device::mojom::blink::XRTrackedImage::New();
    *session_options->tracked_images[i] = query.TrackedImages()[i];
  }

  if (query.HasFeature(device::mojom::XRSessionFeature::DEPTH)) {
    session_options->depth_options =
        device::mojom::blink::XRDepthOptions::New();
    session_options->depth_options->usage_preferences = query.PreferredUsage();
    session_options->depth_options->data_format_preferences =
        query.PreferredFormat();
    session_options->depth_options->depth_type_request =
        query.DepthTypeRequest();
    session_options->depth_options->match_depth_view = query.MatchDepthView();
  }

  session_options->trace_id = query.TraceId();

  return session_options;
}

const char XRSystem::kSupplementName[] = "XRSystem";

XRSystem* XRSystem::FromIfExists(Document& document) {
  if (!document.domWindow())
    return nullptr;
  return Supplement<Navigator>::From<XRSystem>(
      document.domWindow()->navigator());
}

XRSystem* XRSystem::From(Document& document) {
  DVLOG(2) << __func__;

  return document.domWindow() ? xr(*document.domWindow()->navigator())
                              : nullptr;
}

XRSystem* XRSystem::xr(Navigator& navigator) {
  DVLOG(2) << __func__;

  LocalDOMWindow* window = navigator.DomWindow();
  if (!window)
    return nullptr;

  XRSystem* xr = Supplement<Navigator>::From<XRSystem>(navigator);
  if (!xr) {
    xr = MakeGarbageCollected<XRSystem>(navigator);
    ProvideTo(navigator, xr);

    ukm::builders::XR_WebXR(window->UkmSourceID())
        .SetDidUseNavigatorXR(1)
        .Record(window->UkmRecorder());
  }
  return xr;
}

XRSystem::XRSystem(Navigator& navigator)
    : Supplement<Navigator>(navigator),
      ExecutionContextLifecycleObserver(navigator.DomWindow()),
      FocusChangedObserver(navigator.DomWindow()->GetFrame()->GetPage()),
      service_(navigator.DomWindow()),
      environment_provider_(navigator.DomWindow()),
      receiver_(this, navigator.DomWindow()),
      navigation_start_(navigator.DomWindow()
                            ->document()
                            ->Loader()
                            ->GetTiming()
                            .NavigationStart()),
      webxr_internals_renderer_listener_(GetExecutionContext()) {
  if (!base::FeatureList::IsEnabled(kWebXrAttributeAllowsBackForwardCache)) {
    DisableBackForwardCache();
  }
}

void XRSystem::FocusedFrameChanged() {
  // Tell all sessions that focus changed.
  // Since this eventually dispatches an event to the page, the page could
  // create a new session which would invalidate our iterators; so iterate over
  // a copy of the session map.
  HeapHashSet<WeakMember<XRSession>> processing_sessions = sessions_;
  for (const auto& session : processing_sessions) {
    session->OnFocusChanged();
  }

  if (frame_provider_)
    frame_provider_->OnFocusChanged();
}

bool XRSystem::IsFrameFocused() {
  return FocusChangedObserver::IsFrameFocused(
      DomWindow() ? DomWindow()->GetFrame() : nullptr);
}

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_.Get();
}

device::mojom::blink::XREnvironmentIntegrationProvider*
XRSystem::xrEnvironmentProviderRemote() {
  return environment_provider_.get();
}

device::mojom::blink::VRService* XRSystem::BrowserService() {
  return service_.get();
}

void XRSystem::AddEnvironmentProviderErrorHandler(
    EnvironmentProviderErrorCallback callback) {
  environment_provider_error_callbacks_.push_back(std::move(callback));
}

void XRSystem::ExitPresent(base::OnceClosure on_exited) {
  DVLOG(1) << __func__;

  // Make sure to clear any fullscreen states on exiting the session. This is
  // necessary not only in case the device side did not clean up the fullscreen
  // state properly, but also 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 may be allowed
  //   because doc->IsXrOverlay() is still true at this point
  // - renderer processes XR session shutdown (this method)
  // - browser re-enters fullscreen unexpectedly.
  if (LocalDOMWindow* window = DomWindow()) {
    Document* doc = window->document();
    Element* fullscreen_element = Fullscreen::FullscreenElementFrom(*doc);
    DVLOG(3) << __func__ << ": fullscreen_element=" << fullscreen_element;
    if (fullscreen_element) {
      fullscreen_exit_observer_ =
          MakeGarbageCollected<XrExitFullscreenObserver>();
      // Once we exit fullscreen, we'll need to come back here to finish
      // shutting down the session.
      fullscreen_exit_observer_->ExitFullscreen(
          doc, blink::BindOnce(&XRSystem::ExitPresent, WrapWeakPersistent(this),
                               std::move(on_exited)));
      return;
    }
  }

  if (service_.is_bound()) {
    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_.is_bound());
    service_->SetFramesThrottled(throttled);
  }
}

ScriptPromise<IDLBoolean> XRSystem::isSessionSupported(
    ScriptState* script_state,
    const V8XRSessionMode& mode,
    ExceptionState& exception_state) {
  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<IDLBoolean>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  if (!GetExecutionContext()) {
    // Reject if the context is inaccessible.
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      kNavigatorDetachedError);
    return promise;  // Promise will be rejected by generated bindings
  }

  DisableBackForwardCache();

  device::mojom::blink::XRSessionMode session_mode =
      V8EnumToSessionMode(mode.AsEnum());
  PendingSupportsSessionQuery* query =
      MakeGarbageCollected<PendingSupportsSessionQuery>(resolver, session_mode);

  if (session_mode == device::mojom::blink::XRSessionMode::kImmersiveAr &&
      !IsImmersiveArAllowed()) {
    DVLOG(2) << __func__
             << ": Immersive AR session is only supported if WebXRARModule "
                "feature is enabled by a runtime feature and web settings";
    query->Resolve(false);
    return promise;
  }

  if (session_mode == device::mojom::blink::XRSessionMode::kInline) {
    // inline sessions are always supported.
    query->Resolve(true);
    return promise;
  }

  if (!GetExecutionContext()->IsFeatureEnabled(
          network::mojom::PermissionsPolicyFeature::kWebXr,
          ReportOptions::kReportOnFailure)) {
    // Only allow the call to be made if the appropriate permissions policy is
    // in place.
    query->RejectWithSecurityError(kFeaturePolicyBlocked, &exception_state);
    return promise;
  }

  // If TryEnsureService() doesn't set |service_|, then we don't have any WebXR
  // hardware, so we need to reject as being unsupported.
  TryEnsureService();
  if (!service_.is_bound()) {
    query->Resolve(false, &exception_state);
    return promise;
  }

  device::mojom::blink::XRSessionOptionsPtr session_options =
      device::mojom::blink::XRSessionOptions::New();
  session_options->mode = query->mode();
  session_options->trace_id = query->TraceId();

  outstanding_support_queries_.insert(query);
  service_->SupportsSession(
      std::move(session_options),
      BindOnce(&XRSystem::OnSupportsSessionReturned, WrapPersistent(this),
               WrapPersistent(query)));

  return promise;
}

void XRSystem::AddConsoleMessage(mojom::blink::ConsoleMessageLevel error_level,
                                 const String& message) {
  DVLOG(2) << __func__ << ": error_level=" << error_level
           << ", message=" << message;

  if ((error_level == mojom::blink::ConsoleMessageLevel::kError ||
       error_level == mojom::blink::ConsoleMessageLevel::kWarning) &&
      frameProvider()->immersive_session()) {
    AddWebXrInternalsMessage(message);
  }
  GetExecutionContext()->AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>(
      mojom::blink::ConsoleMessageSource::kJavaScript, error_level, message));
}

void XRSystem::AddWebXrInternalsMessage(const String& message) {
  if (webxr_internals_renderer_listener_) {
    device::mojom::blink::XrLogMessagePtr xr_logging_statistics =
        device::mojom::blink::XrLogMessage::New();

    xr_logging_statistics->message = message;
    xr_logging_statistics->trace_id =
        frameProvider()->immersive_session()->GetTraceId();

    webxr_internals_renderer_listener_->OnConsoleLog(
        std::move(xr_logging_statistics));
  }
}

void XRSystem::RequestSessionInternal(
    device::mojom::blink::XRSessionMode session_mode,
    PendingRequestSessionQuery* query,
    ExceptionState* exception_state) {
  // The various session request methods may have other checks that would reject
  // before needing to create the vr service, so we don't try to create it here.
  switch (session_mode) {
    case device::mojom::blink::XRSessionMode::kImmersiveVr:
    case device::mojom::blink::XRSessionMode::kImmersiveAr:
      RequestImmersiveSession(query, exception_state);
      break;
    case device::mojom::blink::XRSessionMode::kInline:
      RequestInlineSession(query, exception_state);
      break;
  }
}

void XRSystem::RequestImmersiveSession(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(DomWindow()->UkmSourceID())
        .SetDidRequestPresentation(1)
        .Record(DomWindow()->UkmRecorder());
    did_log_request_immersive_session_ = true;
  }

  // Make sure the request is allowed
  auto* immersive_session_request_error =
      CheckImmersiveSessionRequestAllowed(DomWindow());
  if (immersive_session_request_error) {
    DVLOG(2) << __func__
             << ": rejecting session - immersive session not allowed, reason: "
             << 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()) {
    DVLOG(2) << __func__
             << ": rejecting session - immersive session request is already "
                "pending or an immersive session is already active";
    query->RejectWithDOMException(DOMExceptionCode::kInvalidStateError,
                                  kActiveImmersiveSession, exception_state);
    return;
  }

  // If TryEnsureService() doesn't set |service_|, then we don't have any WebXR
  // hardware.
  TryEnsureService();
  if (!service_.is_bound()) {
    DVLOG(2) << __func__ << ": rejecting session - service is not bound";
    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,
                                  kInvalidRequiredFeatures, 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);

  // If we're already in fullscreen mode, we need to exit and re-enter
  // fullscreen mode to properly apply the is_xr_overlay property and reset the
  // existing navigationUI options that may be conflicting with what we want.
  // Request a fullscreen exit, and continue with the session request once that
  // completes.
  Document* doc = DomWindow()->document();
  if (Fullscreen::FullscreenElementFrom(*doc)) {
    fullscreen_exit_observer_ =
        MakeGarbageCollected<XrExitFullscreenObserver>();

    base::OnceClosure callback =
        BindOnce(&XRSystem::DoRequestSession, WrapWeakPersistent(this),
                 WrapPersistent(query), std::move(session_options));
    fullscreen_exit_observer_->ExitFullscreen(doc, std::move(callback));
    return;
  }

  DoRequestSession(std::move(query), std::move(session_options));
}

void XRSystem::DoRequestSession(
    PendingRequestSessionQuery* query,
    device::mojom::blink::XRSessionOptionsPtr session_options) {
  service_->RequestSession(
      std::move(session_options),
      BindOnce(&XRSystem::OnRequestSessionReturned, WrapWeakPersistent(this),
               WrapPersistent(query)));
}

void XRSystem::RequestInlineSession(PendingRequestSessionQuery* query,
                                    ExceptionState* exception_state) {
  DVLOG(2) << __func__;
  // Make sure the inline session request was allowed
  auto* inline_session_request_error =
      CheckInlineSessionRequestAllowed(DomWindow()->GetFrame(), *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()) {
    DVLOG(2) << __func__ << ": rejecting session - invalid required features";
    query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError,
                                  kInvalidRequiredFeatures, exception_state);
    return;
  }

  auto sensor_requirement = query->GetSensorRequirement();

  // Try to get the service now. If we can't get it, then we know that we can
  // only support a sensorless session. But if we *can* get it, then we need to
  // check if we have any hardware that supports the requested features.
  TryEnsureService();

  // 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_.is_bound() &&
       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_.is_bound()) {
    DVLOG(2) << __func__ << ": rejecting session - no service";
    query->RejectWithDOMException(DOMExceptionCode::kNotSupportedError,
                                  kNoDevicesMessage, 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),
      BindOnce(&XRSystem::OnRequestSessionReturned, WrapWeakPersistent(this),
               WrapPersistent(query)));
}

XRSystem::RequestedXRSessionFeatureSet XRSystem::ParseRequestedFeatures(
    const Vector<String>& features,
    const device::mojom::blink::XRSessionMode& session_mode,
    XRSessionInit* session_init,
    mojom::blink::ConsoleMessageLevel error_level) {
  DVLOG(2) << __func__ << ": features.size()=" << features.size()
           << ", session_mode=" << session_mode;
  RequestedXRSessionFeatureSet result;

  // Iterate over all requested features, even if intermediate
  // elements are found to be invalid.
  for (const auto& feature_string : features) {
    auto feature_enum = StringToXRSessionFeature(feature_string);

    if (!feature_enum) {
      AddConsoleMessage(error_level, StrCat({"Unrecognized feature requested: ",
                                             feature_string}));
      result.invalid_features = true;
    } else if (!IsFeatureEnabledForContext(feature_enum.value(),
                                           GetExecutionContext())) {
      AddConsoleMessage(error_level, StrCat({"Unsupported feature requested: ",
                                             feature_string}));
      result.invalid_features = true;
    } else if (!IsFeatureValidForMode(feature_enum.value(), session_mode,
                                      session_init, GetExecutionContext(),
                                      error_level)) {
      AddConsoleMessage(
          error_level,
          StrCat({"Feature '", feature_string, "' is not supported for mode: ",
                  SessionModeToString(session_mode)}));
      result.invalid_features = true;
    } else if (!HasRequiredPermissionsPolicy(GetExecutionContext(),
                                             feature_enum.value())) {
      AddConsoleMessage(error_level,
                        StrCat({"Feature '", feature_string,
                                "' is not permitted by permissions policy"}));
      result.invalid_features = true;
    } else {
      DVLOG(3) << __func__ << ": Adding feature " << feature_string
               << " to valid_features.";
      result.valid_features.insert(feature_enum.value());
    }
  }

  DVLOG(2) << __func__
           << ": result.invalid_features=" << result.invalid_features
           << ", result.valid_features.size()=" << result.valid_features.size();
  return result;
}

ScriptPromise<XRSession> XRSystem::requestSession(
    ScriptState* script_state,
    const V8XRSessionMode& 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.
  if (!DomWindow()) {
    // Reject if the window is inaccessible.
    DVLOG(1) << __func__ << ": DomWindow 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 EmptyPromise();  // Will be rejected by generated
                            // bindings
  }

  DisableBackForwardCache();

  device::mojom::blink::XRSessionMode session_mode =
      V8EnumToSessionMode(mode.AsEnum());

  // If the request is for immersive-ar, ensure that feature is enabled.
  if (session_mode == device::mojom::blink::XRSessionMode::kImmersiveAr &&
      !IsImmersiveArAllowed()) {
    DVLOG(1) << __func__ << ": Immersive AR not allowed";
    exception_state.ThrowTypeError(kImmersiveArModeNotValid);

    // 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(DomWindow()->UkmSourceID())
        .SetMode(static_cast<int64_t>(session_mode))
        .SetStatus(static_cast<int64_t>(SessionRequestStatus::kOtherError))
        .Record(DomWindow()->UkmRecorder());
    return EmptyPromise();
  }

  // Parse required feature strings
  RequestedXRSessionFeatureSet required_features;
  if (session_init && session_init->hasRequiredFeatures()) {
    required_features = ParseRequestedFeatures(
        session_init->requiredFeatures(), session_mode, session_init,
        mojom::blink::ConsoleMessageLevel::kError);
  }

  // Parse optional feature strings
  RequestedXRSessionFeatureSet optional_features;
  if (session_init && session_init->hasOptionalFeatures()) {
    optional_features = ParseRequestedFeatures(
        session_init->optionalFeatures(), session_mode, session_init,
        mojom::blink::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 (HasRequiredPermissionsPolicy(GetExecutionContext(), feature)) {
      required_features.valid_features.insert(feature);
    } else {
      DVLOG(2) << __func__
               << ": permissions policy not satisfied for a default feature: "
               << feature;
      AddConsoleMessage(
          mojom::blink::ConsoleMessageLevel::kError,
          StrCat({"Permissions policy is not satisfied for feature '",
                  XRSessionFeatureToString(feature),
                  "' please ensure that appropriate permissions policy is "
                  "enabled."}));
      required_features.invalid_features = true;
    }
  }

  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver<XRSession>>(
      script_state, exception_state.GetContext());
  auto promise = resolver->Promise();

  PendingRequestSessionQuery* query =
      MakeGarbageCollected<PendingRequestSessionQuery>(
          DomWindow()->UkmSourceID(), resolver, session_mode,
          std::move(required_features), std::move(optional_features));

  if (query->HasFeature(device::mojom::XRSessionFeature::DOM_OVERLAY)) {
    // Prerequisites were checked by IsFeatureValidForMode and IDL.
    DCHECK(session_init);
    DCHECK(session_init->hasDomOverlay());
    DCHECK(session_init->domOverlay()->hasRoot()) << "required in IDL";
    query->SetDOMOverlayElement(session_init->domOverlay()->root());
  }

  if (query->HasFeature(device::mojom::XRSessionFeature::IMAGE_TRACKING)) {
    // Prerequisites were checked by IsFeatureValidForMode.
    DCHECK(session_init);
    DCHECK(session_init->hasTrackedImages());
    DVLOG(3) << __func__ << ": set up trackedImages";
    Vector<device::mojom::blink::XRTrackedImage> images;
    int index = 0;
    for (auto& image : session_init->trackedImages()) {
      DCHECK(image->hasImage()) << "required in IDL";
      DCHECK(image->hasWidthInMeters()) << "required in IDL";
      if (std::isnan(image->widthInMeters()) ||
          image->widthInMeters() <= 0.0f) {
        String message =
            StrCat({"trackedImages[", String::Number(index),
                    "].widthInMeters invalid, must be a positive number."});
        query->RejectWithTypeError(message, &exception_state);
        return promise;
      }
      // Extract an SkBitmap snapshot for each image.
      scoped_refptr<StaticBitmapImage> static_bitmap_image =
          image->image()->BitmapImage();
      SkBitmap sk_bitmap = static_bitmap_image->AsSkBitmapForCurrentFrame(
          kRespectImageOrientation);
      images.emplace_back(sk_bitmap, static_bitmap_image->Size(),
                          image->widthInMeters());
      ++index;
    }
    query->SetTrackedImages(images);
  }

  if (query->HasFeature(device::mojom::XRSessionFeature::DEPTH)) {
    // Prerequisites were checked by IsFeatureValidForMode and IDL.
    DCHECK(session_init);
    DCHECK(session_init->hasDepthSensing());
    DCHECK(session_init->depthSensing()->hasUsagePreference())
        << "required in IDL";
    DCHECK(session_init->depthSensing()->hasDataFormatPreference())
        << "required in IDL";

    Vector<device::mojom::XRDepthUsage> preferred_usage;
    std::ranges::transform(session_init->depthSensing()->usagePreference(),
                           std::back_inserter(preferred_usage),
                           ParseDepthUsage);

    Vector<device::mojom::XRDepthDataFormat> preferred_format;
    std::ranges::transform(session_init->depthSensing()->dataFormatPreference(),
                           std::back_inserter(preferred_format),
                           ParseDepthFormat);

    // `match_depth_view` is true if it is not set.
    bool match_depth_view = true;
    Vector<device::mojom::XRDepthType> type_request;
    if (session_init->depthSensing()->hasMatchDepthView()) {
      match_depth_view = session_init->depthSensing()->matchDepthView();
    }

    if (session_init->depthSensing()->hasDepthTypeRequest()) {
      std::ranges::transform(session_init->depthSensing()->depthTypeRequest(),
                             std::back_inserter(type_request), ParseDepthType);
    }

    query->SetDepthSensingConfiguration(preferred_usage, preferred_format,
                                        type_request, match_depth_view);
  }

  // Defer to request the session until the prerendering page is activated.
  if (DomWindow()->document()->IsPrerendering()) {
    // Pass a nullptr instead of |exception_state| because we can't guarantee
    // this object is alive until the prerendering page is activate.
    DomWindow()->document()->AddPostPrerenderingActivationStep(BindOnce(
        &XRSystem::RequestSessionInternal, WrapWeakPersistent(this),
        session_mode, WrapPersistent(query), /*exception_state=*/nullptr));
    return promise;
  }

  RequestSessionInternal(session_mode, query, &exception_state);
  return promise;
}

void XRSystem::MakeXrCompatibleAsync(
    device::mojom::blink::VRService::MakeXrCompatibleCallback callback) {
  if (!GetExecutionContext()->IsFeatureEnabled(
          network::mojom::PermissionsPolicyFeature::kWebXr)) {
    std::move(callback).Run(
        device::mojom::XrCompatibleResult::kWebXrFeaturePolicyBlocked);
    return;
  }

  TryEnsureService();
  if (service_.is_bound()) {
    service_->MakeXrCompatible(std::move(callback));
  } else {
    std::move(callback).Run(
        device::mojom::XrCompatibleResult::kNoDeviceAvailable);
  }
}

void XRSystem::MakeXrCompatibleSync(
    device::mojom::XrCompatibleResult* xr_compatible_result) {
  if (!GetExecutionContext()->IsFeatureEnabled(
          network::mojom::PermissionsPolicyFeature::kWebXr)) {
    *xr_compatible_result =
        device::mojom::XrCompatibleResult::kWebXrFeaturePolicyBlocked;
    return;
  }
  *xr_compatible_result = device::mojom::XrCompatibleResult::kNoDeviceAvailable;

  TryEnsureService();
  if (service_.is_bound())
    service_->MakeXrCompatible(xr_compatible_result);
}

void XRSystem::OnSessionEnded(XRSession* session) {
  if (session->immersive()) {
    webxr_internals_renderer_listener_.reset();
  }
}

// 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() {
  ExecutionContext* context = GetExecutionContext();
  if (context && context->IsFeatureEnabled(
                     network::mojom::PermissionsPolicyFeature::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::OnRequestSessionReturned(
    PendingRequestSessionQuery* query,
    device::mojom::blink::RequestSessionResultPtr result) {
  // If session creation failed, move straight on to processing that.
  if (!result->is_success()) {
    FinishSessionCreation(query, std::move(result));
    return;
  }

  Element* fullscreen_element = nullptr;
  const auto& enabled_features =
      result->get_success()->session->enabled_features;
  if (std::ranges::contains(enabled_features,
                            device::mojom::XRSessionFeature::DOM_OVERLAY)) {
    fullscreen_element = query->DOMOverlayElement();
  }

  // Only setup for dom_overlay if the query actually had a DOMOverlayElement
  // and the session enabled dom_overlay. (Note that fullscreen_element will be
  // null if the feature was not enabled).
  bool setup_for_dom_overlay = !!fullscreen_element;

// On Android, due to the way the device renderer is configured, we may need
// to enter fullscreen, so if we aren't supposed to enter DOMOverlay, we simply
// fullscreen the document body.
#if BUILDFLAG(IS_ANDROID)
  if (!fullscreen_element && result->get_success()->session->wants_fullscreen) {
    fullscreen_element = DomWindow()->document()->body();
  }
#endif

  // If we don't need to enter fullscreen continue with session setup.
  if (!fullscreen_element) {
    FinishSessionCreation(query, std::move(result));
    return;
  }

  const bool session_has_camera_access = std::ranges::contains(
      enabled_features, device::mojom::XRSessionFeature::CAMERA_ACCESS);

  // At this point, we know that we have an element that we need to make
  // fullscreen, so we do that before we continue setting up the session.
  fullscreen_enter_observer_ =
      MakeGarbageCollected<XrEnterFullscreenObserver>();
  fullscreen_enter_observer_->RequestFullscreen(
      fullscreen_element, setup_for_dom_overlay, session_has_camera_access,
      BindOnce(&XRSystem::OnFullscreenConfigured, WrapPersistent(this),
               WrapPersistent(query), std::move(result)));
}

void XRSystem::OnFullscreenConfigured(
    PendingRequestSessionQuery* query,
    device::mojom::blink::RequestSessionResultPtr result,
    bool fullscreen_succeeded) {
  // At this point we no longer need the enter observer, so go ahead and destroy
  // it.
  fullscreen_enter_observer_ = nullptr;

  if (fullscreen_succeeded) {
    FinishSessionCreation(query, std::move(result));
  } else {
    FinishSessionCreation(
        query, device::mojom::blink::RequestSessionResult::NewFailureReason(
                   device::mojom::RequestSessionError::FULLSCREEN_ERROR));
  }
}

void XRSystem::FinishSessionCreation(
    PendingRequestSessionQuery* query,
    device::mojom::blink::RequestSessionResultPtr result) {
  DVLOG(2) << __func__;
  // 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;
  }

  if (!result->is_success()) {
    // |service_| does not support the requested mode. Attempt to create a
    // sensorless session.
    if (query->GetSensorRequirement() != SensorRequirement::kRequired) {
      DVLOG(2) << __func__ << ": session creation failed - creating sensorless";
      XRSession* session = CreateSensorlessInlineSession();
      query->Resolve(session);
      return;
    }

    String error_message =
        StrCat({"Could not create a session because: ",
                GetConsoleMessage(result->get_failure_reason())});
    AddConsoleMessage(mojom::blink::ConsoleMessageLevel::kError, error_message);
    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);

  XRSessionFeatureSet enabled_features;
  for (const auto& feature : session_ptr->enabled_features) {
    DVLOG(2) << __func__ << ": feature " << feature << " will be enabled";
    enabled_features.insert(feature);
  }

  XRSession* session = CreateSession(
      query->mode(), session_ptr->enviroment_blend_mode,
      session_ptr->interaction_mode, std::move(session_ptr->client_receiver),
      std::move(session_ptr->device_config), enabled_features,
      result->get_success()->trace_id);

  frameProvider()->OnSessionStarted(session, std::move(session_ptr));

  // The session is immersive, so we need to set up the WebXR Internals
  // listener.
  if (session->immersive() && result->get_success()->xr_internals_listener) {
    webxr_internals_renderer_listener_.Bind(
        std::move(std::move(result->get_success()->xr_internals_listener)),
        GetExecutionContext()->GetTaskRunner(TaskType::kInternalDefault));
  }

  if (query->mode() == device::mojom::blink::XRSessionMode::kImmersiveVr ||
      query->mode() == device::mojom::blink::XRSessionMode::kImmersiveAr) {
    const bool anchors_enabled =
        enabled_features.Contains(device::mojom::XRSessionFeature::ANCHORS);
    const bool hit_test_enabled =
        enabled_features.Contains(device::mojom::XRSessionFeature::HIT_TEST);
    const bool environment_integration = hit_test_enabled || anchors_enabled;
    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(
          BindOnce(&XRSystem::OnEnvironmentProviderDisconnect,
                   WrapWeakPersistent(this)));

      session->OnEnvironmentProviderCreated();
    }

    auto dom_overlay_feature = device::mojom::XRSessionFeature::DOM_OVERLAY;
    if (query->mode() == device::mojom::blink::XRSessionMode::kImmersiveAr &&
        query->HasFeature(dom_overlay_feature) &&
        enabled_features.Contains(dom_overlay_feature)) {
      DCHECK(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.
      session->SetDOMOverlayElement(query->DOMOverlayElement());
    }
  }

  UseCounter::Count(ExecutionContext::From(query->GetScriptState()),
                    WebFeature::kWebXrSessionCreated);

  if (query->mode() == device::mojom::blink::XRSessionMode::kImmersiveAr) {
    UseCounter::CountWebDXFeature(
        ExecutionContext::From(query->GetScriptState()),
        WebDXFeature::kWebxrAr);
  }

  query->Resolve(session, std::move(metrics_recorder));

  // Do this after the resolve is sent so that the page can subscribe to any
  // events. Then send off any events we want to fire off immediately.
  session->DispatchInitialEvents();
}

void XRSystem::AddedEventListener(
    const AtomicString& event_type,
    RegisteredEventListener& registered_listener) {
  EventTarget::AddedEventListener(event_type, registered_listener);

  // If we're adding an event listener we should spin up the service, if we can,
  // so that we can actually register for notifications.
  TryEnsureService();
  if (!service_.is_bound())
    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,
    device::mojom::blink::XREnvironmentBlendMode blend_mode,
    device::mojom::blink::XRInteractionMode interaction_mode,
    mojo::PendingReceiver<device::mojom::blink::XRSessionClient>
        client_receiver,
    device::mojom::blink::XRSessionDeviceConfigPtr device_config,
    XRSessionFeatureSet enabled_features,
    uint64_t trace_id,
    bool sensorless_session) {
  XRSession* session = MakeGarbageCollected<XRSession>(
      this, std::move(client_receiver), mode, blend_mode, interaction_mode,
      std::move(device_config), sensorless_session, std::move(enabled_features),
      trace_id);
  sessions_.insert(session);
  return session;
}

XRSession* XRSystem::CreateSensorlessInlineSession() {
  // TODO(https://crbug.com/944936): The blend mode could be "additive".
  device::mojom::blink::XREnvironmentBlendMode blend_mode =
      device::mojom::blink::XREnvironmentBlendMode::kOpaque;
  device::mojom::blink::XRInteractionMode interaction_mode =
      device::mojom::blink::XRInteractionMode::kScreenSpace;
  device::mojom::blink::XRSessionDeviceConfigPtr device_config =
      device::mojom::blink::XRSessionDeviceConfig::New();
  return CreateSession(device::mojom::blink::XRSessionMode::kInline, blend_mode,
                       interaction_mode,
                       mojo::NullReceiver() /* client receiver */,
                       std::move(device_config),
                       {device::mojom::XRSessionFeature::REF_SPACE_VIEWER},
                       kInvalidTraceId, true /* sensorless_session */);
}

void XRSystem::Dispose(DisposeType dispose_type) {
  switch (dispose_type) {
    case DisposeType::kContextDestroyed:
      is_context_destroyed_ = true;
      break;
    case DisposeType::kDisconnected:
      did_service_ever_disconnect_ = true;
      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_.empty());

  HeapHashSet<Member<PendingRequestSessionQuery>> request_queries =
      outstanding_request_queries_;
  for (const auto& query : request_queries) {
    OnRequestSessionReturned(
        query, device::mojom::blink::RequestSessionResult::NewFailureReason(
                   device::mojom::RequestSessionError::INVALID_CLIENT));
  }
  DCHECK(outstanding_support_queries_.empty());
}

void XRSystem::OnEnvironmentProviderDisconnect() {
  for (auto& callback : environment_provider_error_callbacks_) {
    std::move(callback).Run();
  }

  environment_provider_error_callbacks_.clear();
  environment_provider_.reset();
}

void XRSystem::TryEnsureService() {
  DVLOG(2) << __func__;

  // If we already have a service, there's nothing to do.
  if (service_.is_bound()) {
    DVLOG(2) << __func__ << ": service already bound";
    return;
  }

  // If the service has been disconnected in the past or our context has been
  // destroyed, don't try to get the service again.
  if (did_service_ever_disconnect_ || is_context_destroyed_) {
    DVLOG(2) << __func__
             << ": service disconnected or context destroyed, "
                "did_service_ever_disconnect_="
             << did_service_ever_disconnect_
             << ", is_context_destroyed_=" << is_context_destroyed_;
    return;
  }

  // If the current frame isn't attached, don't try to get the service.
  if (!DomWindow()) {
    DVLOG(2) << ": current frame is not attached";
    return;
  }

  // See https://bit.ly/2S0zRAS for task types.
  DomWindow()->GetBrowserInterfaceBroker().GetInterface(
      service_.BindNewPipeAndPassReceiver(
          DomWindow()->GetTaskRunner(TaskType::kMiscPlatformAPI)));
  service_.set_disconnect_handler(BindOnce(&XRSystem::Dispose,
                                           WrapWeakPersistent(this),
                                           DisposeType::kDisconnected));
}

bool XRSystem::IsImmersiveArAllowed() {
  const bool ar_allowed_in_settings =
      IsImmersiveArAllowedBySettings(DomWindow());

  DVLOG(2) << __func__ << ": ar_allowed_in_settings=" << ar_allowed_in_settings;

  return ar_allowed_in_settings;
}

void XRSystem::DisableBackForwardCache() {
  if (feature_handle_for_scheduler_) {
    return;
  }
  feature_handle_for_scheduler_ =
      DomWindow()->GetFrame()->GetFrameScheduler()->RegisterFeature(
          SchedulingPolicy::Feature::kWebXR,
          SchedulingPolicy{SchedulingPolicy::DisableBackForwardCache()});
}

void XRSystem::Trace(Visitor* visitor) const {
  visitor->Trace(frame_provider_);
  visitor->Trace(sessions_);
  visitor->Trace(service_);
  visitor->Trace(environment_provider_);
  visitor->Trace(receiver_);
  visitor->Trace(webxr_internals_renderer_listener_);
  visitor->Trace(outstanding_support_queries_);
  visitor->Trace(outstanding_request_queries_);
  visitor->Trace(fullscreen_enter_observer_);
  visitor->Trace(fullscreen_exit_observer_);
  Supplement<Navigator>::Trace(visitor);
  ExecutionContextLifecycleObserver::Trace(visitor);
  EventTarget::Trace(visitor);
}

device::mojom::blink::WebXrInternalsRendererListener*
XRSystem::GetWebXrInternalsRendererListener() {
  if (!webxr_internals_renderer_listener_) {
    return nullptr;
  }
  return webxr_internals_renderer_listener_.get();
}

}  // namespace blink
