// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/xr/service/vr_service_impl.h"

#include <algorithm>
#include <utility>
#include <vector>

#include "base/containers/to_vector.h"
#include "base/dcheck_is_on.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/stl_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "base/trace_event/common/trace_event_common.h"
#include "build/build_config.h"
#include "components/viz/common/surfaces/frame_sink_id.h"
#include "content/browser/permissions/permission_util.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/xr/metrics/session_metrics_helper.h"
#include "content/browser/xr/service/browser_xr_runtime_impl.h"
#include "content/browser/xr/service/xr_permission_results.h"
#include "content/browser/xr/service/xr_runtime_manager_impl.h"
#include "content/browser/xr/webxr_internals/mojom/webxr_internals.mojom.h"
#include "content/browser/xr/webxr_internals/webxr_internals_handler_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/permission_controller.h"
#include "content/public/browser/permission_descriptor_util.h"
#include "content/public/browser/permission_request_description.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/child_process_id_util.h"
#include "content/public/common/origin_util.h"
#include "content/public/common/page_visibility_state.h"
#include "device/vr/buildflags/buildflags.h"
#include "device/vr/public/cpp/features.h"
#include "device/vr/public/cpp/session_mode.h"
#include "device/vr/public/mojom/vr_service.mojom-shared.h"
#include "device/vr/public/mojom/xr_device.mojom-shared.h"
#include "device/vr/public/mojom/xr_session.mojom-shared.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "third_party/blink/public/mojom/permissions/permission_status.mojom-shared.h"

#if BUILDFLAG(IS_ANDROID)
#include "base/android/device_info.h"
#endif

namespace {

device::mojom::XRRuntimeSessionOptionsPtr GetRuntimeOptions(
    device::mojom::XRSessionOptions* options) {
  device::mojom::XRRuntimeSessionOptionsPtr runtime_options =
      device::mojom::XRRuntimeSessionOptions::New();
  runtime_options->mode = options->mode;
  return runtime_options;
}

// Helper, returns collection of permissions required for XR session creation
// for session with mode set to |mode|. The order in the result does not matter
// as the permissions API does not honor it.
std::vector<blink::PermissionType> GetRequiredPermissionsForMode(
    device::mojom::XRSessionMode mode) {
  std::vector<blink::PermissionType> permissions;

  auto mode_permission = content::XrPermissionResults::GetPermissionFor(mode);
  if (mode_permission) {
    permissions.push_back(*mode_permission);
  }

  return permissions;
}

// Helper, returns collection of permissions required for XR session creation
// for session with enabled features listed in |required_features| and
// |optional_features|. The order in the result does not matter as the
// permissions API does not honor it.
std::vector<blink::PermissionType> GetRequiredPermissionsForFeatures(
    const std::unordered_set<device::mojom::XRSessionFeature>&
        required_features,
    const std::unordered_set<device::mojom::XRSessionFeature>&
        optional_features) {
  std::vector<blink::PermissionType> permissions;

  for (const auto& required_feature : required_features) {
    auto feature_permission =
        content::XrPermissionResults::GetPermissionFor(required_feature);
    if (feature_permission &&
        !std::ranges::contains(permissions, *feature_permission)) {
      permissions.push_back(*feature_permission);
    }
  }

  for (const auto& optional_feature : optional_features) {
    auto feature_permission =
        content::XrPermissionResults::GetPermissionFor(optional_feature);
    if (feature_permission &&
        !std::ranges::contains(permissions, *feature_permission)) {
      permissions.push_back(*feature_permission);
    }
  }

  return permissions;
}

// TODO(crbug.com/40930146): Replace with std::ranges::set_difference
std::unordered_set<device::mojom::XRSessionFeature> GetMissingRequiredFeatures(
    const std::unordered_set<device::mojom::XRSessionFeature>& enabled_features,
    const std::unordered_set<device::mojom::XRSessionFeature>&
        required_features) {
  DVLOG(3) << __func__
           << ": enabled_features.size()=" << enabled_features.size();

  std::unordered_set<device::mojom::XRSessionFeature> missing_required_features;

  for (const auto& required_feature : required_features) {
    if (!enabled_features.contains(required_feature)) {
      DVLOG(2) << __func__
               << ": one of the required features was not enabled on the "
                  "created session, feature: "
               << required_feature;
      missing_required_features.insert(required_feature);
    }
  }

  return missing_required_features;
}

void RejectSession(device::mojom::VRService::RequestSessionCallback callback,
                   size_t trace_id,
                   device::mojom::RequestSessionError error,
                   const std::string& failure_reason_description,
                   std::unordered_set<device::mojom::XRSessionFeature>*
                       rejected_features = nullptr) {
  DVLOG(2) << __func__
           << ": failure reason description=" << failure_reason_description;

  webxr::mojom::SessionRejectedRecordPtr session_rejected_record =
      webxr::mojom::SessionRejectedRecord::New();
  session_rejected_record->trace_id = trace_id;
  session_rejected_record->failure_reason = error;
  session_rejected_record->rejected_time = base::Time::Now();
  session_rejected_record->failure_reason_description =
      failure_reason_description;
  if (rejected_features) {
    session_rejected_record->rejected_features.assign(
        rejected_features->begin(), rejected_features->end());
  }

  auto* runtime_manager_impl = static_cast<content::XRRuntimeManagerImpl*>(
      content::XRRuntimeManager::GetInstanceIfCreated());
  if (runtime_manager_impl) {
    runtime_manager_impl->GetLoggerManager().RecordSessionRejected(
        std::move(session_rejected_record));
  }

  std::move(callback).Run(
      device::mojom::RequestSessionResult::NewFailureReason(error));
}

}  // namespace

namespace content {

VRServiceImpl::SessionRequestData::SessionRequestData(
    device::mojom::XRSessionOptionsPtr options,
    device::mojom::VRService::RequestSessionCallback callback,
    device::mojom::XRDeviceId runtime_id)
    : callback(std::move(callback)),
      required_features(options->required_features.begin(),
                        options->required_features.end()),
      optional_features(options->optional_features.begin(),
                        options->optional_features.end()),
      options(std::move(options)),
      runtime_id(runtime_id) {}

VRServiceImpl::SessionRequestData::~SessionRequestData() {
  // In some cases, we may get dropped before the VRService pipe is closed. In
  // these cases we need to try to ensure that the callback is run or else we
  // hit DCHECKs for dropping the callback without closing the pipe.
  // This most often occurs when the Permissions prompt is dismissed.
  if (callback) {
    RejectSession(std::move(callback), options->trace_id,
                  device::mojom::RequestSessionError::UNKNOWN_FAILURE,
                  "SessionRequestData destroyed without running callback.");
  }
}

VRServiceImpl::SessionRequestData::SessionRequestData(SessionRequestData&&) =
    default;

VRServiceImpl::XrCompatibleCallback::XrCompatibleCallback(
    device::mojom::VRService::MakeXrCompatibleCallback callback)
    : callback(std::move(callback)) {}

VRServiceImpl::XrCompatibleCallback::XrCompatibleCallback(
    XrCompatibleCallback&& wrapper) {
  this->callback = std::move(wrapper.callback);
}

VRServiceImpl::XrCompatibleCallback::~XrCompatibleCallback() {
  if (!callback.is_null())
    std::move(callback).Run(
        device::mojom::XrCompatibleResult::kNoDeviceAvailable);
}

VRServiceImpl::VRServiceImpl(content::RenderFrameHost* render_frame_host)
    : WebContentsObserver(
          content::WebContents::FromRenderFrameHost(render_frame_host)),
      render_frame_host_(render_frame_host),
      in_focused_frame_(render_frame_host->GetView()->HasFocus()) {
  DCHECK(render_frame_host_);
  DVLOG(2) << __func__;

  runtime_manager_ =
      XRRuntimeManagerImpl::GetOrCreateInstance(*GetWebContents());
  runtime_manager_->AddService(this);

  magic_window_controllers_.set_disconnect_handler(base::BindRepeating(
      &VRServiceImpl::OnInlineSessionDisconnected,
      base::Unretained(this)));  // Unretained is OK since the collection is
                                 // owned by VRServiceImpl.
}


VRServiceImpl::~VRServiceImpl() {
  DVLOG(2) << __func__;
  Teardown();
}

void VRServiceImpl::Create(
    content::RenderFrameHost* render_frame_host,
    mojo::PendingReceiver<device::mojom::VRService> receiver) {
  DVLOG(2) << __func__;
  std::unique_ptr<VRServiceImpl> vr_service_impl =
      std::make_unique<VRServiceImpl>(render_frame_host);

  VRServiceImpl* impl = vr_service_impl.get();
  impl->receiver_ = mojo::MakeSelfOwnedReceiver(std::move(vr_service_impl),
                                                std::move(receiver));
}

void VRServiceImpl::InitializationComplete() {
  // After initialization has completed, we can correctly answer
  // supportsSession, and can provide correct display capabilities.
  DVLOG(2) << __func__;
  initialization_complete_ = true;

  ResolvePendingRequests();
}

void VRServiceImpl::SetClient(
    mojo::PendingRemote<device::mojom::VRServiceClient> service_client) {
  if (service_client_) {
    mojo::ReportBadMessage("ServiceClient should only be set once.");
    return;
  }

  DVLOG(2) << __func__;
  service_client_.Bind(std::move(service_client));
}

void VRServiceImpl::ResolvePendingRequests() {
  DVLOG(2) << __func__
           << ": pending_requests_.size()=" << pending_requests_.size();
  for (auto& callback : pending_requests_) {
    std::move(callback).Run();
  }
  pending_requests_.clear();
}

void VRServiceImpl::RuntimesChanged() {
  DVLOG(2) << __func__;
  if (!IsRenderFrameHostVisible()) {
    pending_device_changed_ = true;
    return;
  }
  if (service_client_) {
    service_client_->OnDeviceChanged();
  }
}

void VRServiceImpl::OnWebContentsFocused(content::RenderWidgetHost* host) {
  OnWebContentsFocusChanged(host, true);
}

void VRServiceImpl::OnWebContentsLostFocus(content::RenderWidgetHost* host) {
  OnWebContentsFocusChanged(host, false);
}

void VRServiceImpl::RenderFrameDeleted(content::RenderFrameHost* host) {
  DVLOG(2) << __func__;
  if (host != render_frame_host_)
    return;

  // |Teardown| will clear the `render_frame_host_` and also clean up any state
  // before doing so.
  Teardown();

  // Receiver should always be live here, as this is a SelfOwnedReceiver.
  // Close the receiver (and delete this VrServiceImpl) when the RenderFrameHost
  // is deleted.
  DCHECK(receiver_.get());
  receiver_->Close();
}

void VRServiceImpl::OnVisibilityChanged(content::Visibility visibility) {
  // Re-check frame visibility, as our associated RenderFrameHost may remain
  // hidden even when the top-level page becomes visible.
  if (!pending_device_changed_ || !IsRenderFrameHostVisible()) {
    return;
  }
  pending_device_changed_ = false;
  if (service_client_) {
    service_client_->OnDeviceChanged();
  }
}

void VRServiceImpl::OnWebContentsFocusChanged(content::RenderWidgetHost* host,
                                              bool focused) {
  if (!render_frame_host_ || !render_frame_host_->GetView() ||
      render_frame_host_->GetView()->GetRenderWidgetHost() != host) {
    return;
  }

  in_focused_frame_ = focused;

  for (const auto& controller : magic_window_controllers_)
    controller->SetFrameDataRestricted(!focused);
}

void VRServiceImpl::OnInlineSessionCreated(
    SessionRequestData request,
    device::mojom::XRRuntimeSessionResultPtr session_result) {
  if (!session_result) {
    TRACE_EVENT("xr",
                "VRServiceImpl::OnInlineSessionCreated: no session_result",
                perfetto::Flow::Global(request.options->trace_id));

    RejectSession(std::move(request.callback), request.options->trace_id,
                  device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR,
                  "Runtime did not provide a session.");
    return;
  }

  mojo::Remote<device::mojom::XRSessionController> controller(
      std::move(session_result->controller));
  // Start giving out magic window data if we are focused.
  controller->SetFrameDataRestricted(!in_focused_frame_);

  auto id = magic_window_controllers_.Add(std::move(controller));
  DVLOG(2) << __func__ << ": session_id=" << id.GetUnsafeValue()
           << " runtime_id=" << request.runtime_id;

  auto* session = session_result->session.get();
  std::unordered_set<device::mojom::XRSessionFeature> enabled_features(
      session->enabled_features.begin(), session->enabled_features.end());

  auto missing_required_features =
      GetMissingRequiredFeatures(enabled_features, request.required_features);
  if (!missing_required_features.empty()) {
    // UNKNOWN_FAILURE since a runtime should not return a session if there
    // exists a required feature that was not enabled - this would signify a bug
    // in the runtime.

    TRACE_EVENT(
        "xr",
        "VRServiceImpl::OnInlineSessionCreated: required feature not granted",
        perfetto::Flow::Global(request.options->trace_id));

    RejectSession(std::move(request.callback), request.options->trace_id,
                  device::mojom::RequestSessionError::UNKNOWN_FAILURE,
                  "Required feature not granted.", &missing_required_features);
    return;
  }

  mojo::PendingRemote<device::mojom::XRSessionMetricsRecorder>
      session_metrics_recorder = GetSessionMetricsHelper()->StartInlineSession(
          *(request.options), enabled_features, id.GetUnsafeValue());

  OnSessionCreated(
      std::move(request), std::move(session_result->session),
      std::move(session_metrics_recorder),
      mojo::PendingRemote<device::mojom::WebXrInternalsRendererListener>());
}

void VRServiceImpl::OnImmersiveSessionCreated(
    SessionRequestData request,
    device::mojom::XRRuntimeSessionResultPtr session_result) {
  DCHECK(request.options);
  if (!session_result) {
    TRACE_EVENT("xr",
                "VRServiceImpl::OnImmersiveSessionCreated: no session_result",
                perfetto::Flow::Global(request.options->trace_id));

    RejectSession(std::move(request.callback), request.options->trace_id,
                  device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR,
                  "Runtime did not provide a session.");
    return;
  }

  auto* session = session_result->session.get();
  std::unordered_set<device::mojom::XRSessionFeature> enabled_features(
      session->enabled_features.begin(), session->enabled_features.end());

  auto missing_required_features =
      GetMissingRequiredFeatures(enabled_features, request.required_features);
  if (!missing_required_features.empty()) {
    // UNKNOWN_FAILURE since a runtime should not return a session if there
    // exists a required feature that was not enabled - this would signify a bug
    // in the runtime.

    TRACE_EVENT("xr",
                "VRServiceImpl::OnImmersiveSessionCreated: required feature "
                "not granted",
                perfetto::Flow::Global(request.options->trace_id));

    RejectSession(std::move(request.callback), request.options->trace_id,
                  device::mojom::RequestSessionError::UNKNOWN_FAILURE,
                  "Required feature not granted.", &missing_required_features);
    return;
  }

  if (enabled_features.contains(device::mojom::XRSessionFeature::DOM_OVERLAY)) {
    // Tell RenderFrameHostImpl that we're setting up the WebXR DOM Overlay,
    // it checks for this in EnterFullscreen via HasSeenRecentXrOverlaySetup().
    render_frame_host_->SetIsXrOverlaySetup();
  }

  // Get the metrics tracker for the new immersive session
  mojo::PendingRemote<device::mojom::XRSessionMetricsRecorder>
      session_metrics_recorder =
          GetSessionMetricsHelper()->StartImmersiveSession(
              request.runtime_id, *(request.options), enabled_features);

  render_frame_host_->GetProcess()->OnImmersiveXrSessionStarted();
  has_immersive_session_ = true;

  // If the session specified a FrameSinkId that means that it is handling its
  // own compositing in a way that we should notify the WebContents about.
  if (session_result->frame_sink_id) {
    if (session_result->frame_sink_id->is_valid()) {
      static_cast<WebContentsImpl*>(GetWebContents())
          ->OnXrHasRenderTarget(*session_result->frame_sink_id);
    } else {
      DLOG(ERROR) << __func__ << " frame_sink_id was specified but was invalid";
    }
  }

  OnSessionCreated(std::move(request), std::move(session_result->session),
                   std::move(session_metrics_recorder),
                   runtime_manager_->GetLoggerManager().BindRenderListener());
}

void VRServiceImpl::OnInlineSessionDisconnected(
    mojo::RemoteSetElementId session_id) {
  DVLOG(2) << __func__ << ": session_id=" << session_id.GetUnsafeValue();
  // Notify metrics helper that inline session was stopped.
  auto* metrics_helper = GetSessionMetricsHelper();
  metrics_helper->StopAndRecordInlineSession(session_id.GetUnsafeValue());
}

SessionMetricsHelper* VRServiceImpl::GetSessionMetricsHelper() {
  content::WebContents* web_contents =
      content::WebContents::FromRenderFrameHost(render_frame_host_);
  SessionMetricsHelper* metrics_helper =
      SessionMetricsHelper::FromWebContents(web_contents);
  if (!metrics_helper) {
    // This will only happen if we are not already in VR; set start params
    // accordingly.
    metrics_helper = SessionMetricsHelper::CreateForWebContents(web_contents);
  }

  return metrics_helper;
}

void VRServiceImpl::OnSessionCreated(
    SessionRequestData request,
    device::mojom::XRSessionPtr session,
    mojo::PendingRemote<device::mojom::XRSessionMetricsRecorder>
        session_metrics_recorder,
    mojo::PendingRemote<device::mojom::WebXrInternalsRendererListener>
        xr_internals_listener) {
  DVLOG(2) << __func__ << ": session_runtime_id=" << request.runtime_id;

  // Not checking for validity of |session|, since that's done by
  // |OnInlineSessionCreated| and |OnImmersiveSessionCreated|.

  UMA_HISTOGRAM_ENUMERATION("XR.RuntimeUsed", request.runtime_id);

  TRACE_EVENT("xr", "VRServiceImpl::OnSessionCreated: succeeded",
              perfetto::Flow::Global(request.options->trace_id));

  mojo::Remote<device::mojom::XRSessionClient> client;
  session->client_receiver = client.BindNewPipeAndPassReceiver();

  client->OnVisibilityStateChanged(visibility_state_);
  session_clients_.Add(std::move(client));

  auto success = device::mojom::RequestSessionSuccess::New();
  success->session = std::move(session);
  success->metrics_recorder = std::move(session_metrics_recorder);
  success->trace_id = request.options->trace_id;
  success->xr_internals_listener = std::move(xr_internals_listener);

  std::move(request.callback)
      .Run(device::mojom::RequestSessionResult::NewSuccess(std::move(success)));
}

void VRServiceImpl::RequestSession(
    device::mojom::XRSessionOptionsPtr options,
    device::mojom::VRService::RequestSessionCallback callback) {
  DVLOG(2) << __func__;
  DCHECK(options);

  webxr::mojom::SessionRequestedRecordPtr session_requested_record =
      webxr::mojom::SessionRequestedRecord::New();
  session_requested_record->options = options->Clone();
  session_requested_record->requested_time = base::Time::Now();
  runtime_manager_->GetLoggerManager().RecordSessionRequested(
      std::move(session_requested_record));

  // Queue the request to get to when initialization has completed.
  if (!initialization_complete_) {
    DVLOG(2) << __func__ << ": initialization not yet complete, defer request";
    pending_requests_.push_back(
        base::BindOnce(&VRServiceImpl::RequestSession, base::Unretained(this),
                       std::move(options), std::move(callback)));
    return;
  }

  if (runtime_manager_->IsOtherClientPresenting(this) ||
      runtime_manager_->HasPendingImmersiveRequest()) {
    DVLOG(2) << __func__
             << ": can't create sessions while an immersive session exists";

    // Can't create sessions while an immersive session exists.
    RejectSession(
        std::move(callback), options->trace_id,
        device::mojom::RequestSessionError::EXISTING_IMMERSIVE_SESSION,
        "There is an existing immersive session.");
    return;
  }

  auto* runtime = runtime_manager_->GetRuntimeForOptions(options.get());
  if (!runtime) {
    RejectSession(std::move(callback), options->trace_id,
                  device::mojom::RequestSessionError::NO_RUNTIME_FOUND,
                  "No runtime found for the given session options.");
    return;
  }

  const bool has_user_activation =
      render_frame_host_->HasTransientUserActivation();
  if (!has_user_activation) {
    // User activation is verified blink-side, so this should never fail
    // (everything that happens up to this point should not take enough time for
    // the user activation to expire). Treat lack of user activation as unknown
    // failure:
    RejectSession(std::move(callback), options->trace_id,
                  device::mojom::RequestSessionError::UNKNOWN_FAILURE,
                  "Missing user activation.");
    return;
  }

  if (!IsRenderFrameHostVisible()) {
    // Page visibility is verified blink-side, so this should never fail unless
    // the requesting client is misbehaving or compromised. Treat non-visible
    // page as unknown failure:
    RejectSession(std::move(callback), options->trace_id,
                  device::mojom::RequestSessionError::UNKNOWN_FAILURE,
                  "Page is not visible.");
    return;
  }

  // The consent flow cannot differentiate between optional and required
  // features, but we don't need to block creation if an optional feature is
  // not supported. Remove all unsupported optional features from the
  // optional_features collection before handing it off.
  std::erase_if(options->optional_features, [runtime](auto& feature) {
    return !runtime->SupportsFeature(feature);
  });

  SessionRequestData request(std::move(options), std::move(callback),
                             runtime->GetId());

  GetPermissionStatus(std::move(request), runtime);
}

void VRServiceImpl::DoRequestPermissions(
    const std::vector<blink::PermissionType> request_permissions,
    base::OnceCallback<void(const std::vector<blink::mojom::PermissionStatus>&,
                            bool)> result_callback) {
  PermissionController* permission_controller =
      GetWebContents()->GetBrowserContext()->GetPermissionController();
  CHECK(permission_controller);

  std::vector<blink::mojom::PermissionStatus> current_statuses;
  current_statuses.reserve(request_permissions.size());
  for (auto permission_type : request_permissions) {
    auto descriptor =
        PermissionDescriptorUtil::CreatePermissionDescriptorForPermissionType(
            permission_type);

    blink::mojom::PermissionStatus status;
    if (PermissionUtil::IsDevicePermission(descriptor)) {
      status = permission_controller->GetCombinedPermissionAndDeviceStatus(
          std::move(descriptor), render_frame_host_);
    } else {
      status = permission_controller->GetPermissionStatusForCurrentDocument(
          std::move(descriptor), render_frame_host_);
    }
    current_statuses.push_back(status);
  }

  bool needs_prompt =
      std::ranges::any_of(current_statuses, [](const auto& status) {
        return status == blink::mojom::PermissionStatus::ASK;
      });

  // If we don't need to prompt the user, just return the results now.
  if (!needs_prompt) {
    std::move(result_callback).Run(current_statuses, /*needs_prompt=*/false);
    return;
  }

  permission_controller->RequestPermissionsFromCurrentDocument(
      render_frame_host_,
      PermissionRequestDescription(
          PermissionDescriptorUtil::
              CreatePermissionDescriptorForPermissionTypes(request_permissions),
          /*user_gesture=*/true),
      base::BindOnce(
          [](base::OnceCallback<void(
                 const std::vector<blink::mojom::PermissionStatus>&, bool)>
                 callback,
             bool needs_prompt, const std::vector<PermissionResult>& results) {
            std::move(callback).Run(
                base::ToVector(results, &PermissionResult::status),
                needs_prompt);
          },
          std::move(result_callback), needs_prompt));
}

void VRServiceImpl::GetPermissionStatus(SessionRequestData request,
                                        BrowserXRRuntimeImpl* runtime) {
  DVLOG(2) << __func__;
  DCHECK(request.options);
  DCHECK(runtime);
  DCHECK_EQ(runtime->GetId(), request.runtime_id);

  // Need to calculate the permissions before the call below, as otherwise
  // std::move nulls options out before `GetRequiredPermissions()` runs.
  const std::vector<blink::PermissionType> permissions_for_mode =
      GetRequiredPermissionsForMode(request.options->mode);

  DoRequestPermissions(
      permissions_for_mode,
      base::BindOnce(&VRServiceImpl::OnPermissionResultsForMode,
                     weak_ptr_factory_.GetWeakPtr(), std::move(request),
                     permissions_for_mode));
}

void VRServiceImpl::OnPermissionResultsForMode(
    SessionRequestData request,
    const std::vector<blink::PermissionType>& permissions,
    const std::vector<blink::mojom::PermissionStatus>& results,
    bool needs_prompt) {
  DVLOG(2) << __func__ << ": permissions.size()=" << permissions.size();
  DCHECK_EQ(permissions.size(), results.size());

  if (needs_prompt) {
    // Prolong the user activation since the user may have taken long enough to
    // answer the permission prompts that the transient user activation expired.
    // This is fine to do here, since we enforce that the activation existed
    // prior to requesting permissions.
    DVLOG(3) << __func__ << ": prolonging user activation, current status="
             << render_frame_host_->HasTransientUserActivation();
    render_frame_host_->NotifyUserActivation(
        blink::mojom::UserActivationNotificationType::kInteraction);
  } else {
    DVLOG(3) << __func__
             << ": NOT prolonging user activation (no prompt shown)";
  }

  const XrPermissionResults permission_results(permissions, results);

  bool is_consent_granted =
      permission_results.HasPermissionsFor(request.options->mode);
  DVLOG(2) << __func__ << ": is_consent_granted=" << is_consent_granted;

  if (!is_consent_granted) {
    RejectSession(std::move(request.callback), request.options->trace_id,
                  device::mojom::RequestSessionError::USER_DENIED_CONSENT,
                  "Consent was not granted for the requested mode.");
    return;
  }

  const std::vector<blink::PermissionType> permissions_for_features =
      GetRequiredPermissionsForFeatures(request.required_features,
                                        request.optional_features);

  auto result_callback =
      base::BindOnce(&VRServiceImpl::OnPermissionResultsForFeatures,
                     weak_ptr_factory_.GetWeakPtr(), std::move(request),
                     permissions_for_features);
  if (permissions_for_features.empty()) {
    std::move(result_callback).Run({}, /*needs_prompt=*/false);
    return;
  }

  DoRequestPermissions(permissions_for_features, std::move(result_callback));
}

void VRServiceImpl::OnPermissionResultsForFeatures(
    SessionRequestData request,
    const std::vector<blink::PermissionType>& permissions,
    const std::vector<blink::mojom::PermissionStatus>& results,
    bool needs_prompt) {
  if (needs_prompt) {
    // Prolong the user activation since the user may have taken long enough to
    // answer the permission prompts that the transient user activation expired.
    // This is fine to do here, since we enforce that the activation existed
    // prior to requesting permissions.
    DVLOG(3) << __func__ << ": prolonging user activation, current status="
             << render_frame_host_->HasTransientUserActivation();
    render_frame_host_->NotifyUserActivation(
        blink::mojom::UserActivationNotificationType::kInteraction);
  } else {
    DVLOG(3) << __func__
             << ": NOT prolonging user activation (no prompt shown)";
  }

  const XrPermissionResults permission_results(permissions, results);

  std::unordered_set<device::mojom::XRSessionFeature> rejected_features;
  for (auto& required_feature : request.required_features) {
    if (!permission_results.HasPermissionsFor(required_feature)) {
      DVLOG(1) << __func__ << ": required_feature=" << required_feature
               << " lacks neccessary permissions";

      rejected_features.insert(required_feature);
    }
  }

  if (!rejected_features.empty()) {
    RejectSession(std::move(request.callback), request.options->trace_id,
                  device::mojom::RequestSessionError::USER_DENIED_CONSENT,
                  "Lacks necessary permissions for the required feature.",
                  &rejected_features);
    return;
  }

  std::unordered_set<device::mojom::XRSessionFeature> granted_optional_features;

  for (auto& optional_feature : request.optional_features) {
    if (permission_results.HasPermissionsFor(optional_feature)) {
      granted_optional_features.insert(optional_feature);
    } else {
      DVLOG(2) << __func__ << ": optional_feature=" << optional_feature
               << " lacks neccessary permissions";
    }
  }

  // Replace optional features on the request with the ones that have been
  // granted by the user:
  std::swap(request.optional_features, granted_optional_features);

  // Re-check for another client instance after a potential user consent.
  if (runtime_manager_->IsOtherClientPresenting(this)) {
    // Can't create sessions while an immersive session exists.
    RejectSession(
        std::move(request.callback), request.options->trace_id,
        device::mojom::RequestSessionError::EXISTING_IMMERSIVE_SESSION,
        "Another client started presenting while waiting for permissions.");
    return;
  }

  EnsureRuntimeInstalled(std::move(request), nullptr);
}

void VRServiceImpl::EnsureRuntimeInstalled(SessionRequestData request,
                                           BrowserXRRuntimeImpl* runtime) {
  DVLOG(2) << __func__;

  // If we were not provided the runtime, try to get it again.
  if (!runtime)
    runtime = runtime_manager_->GetRuntimeForOptions(request.options.get());

  // Ensure that it's the same runtime as the one we expect.
  if (!runtime || runtime->GetId() != request.runtime_id) {
    DVLOG(1) << __func__
             << ": failed to obtain the runtime or the runtime id does not "
                "match the expected ID, request.runtime_id="
             << request.runtime_id;

    RejectSession(std::move(request.callback), request.options->trace_id,
                  device::mojom::RequestSessionError::RUNTIMES_CHANGED,
                  "failed to obtain the runtime or the runtime id does not "
                  "match the expected ID.");
    return;
  }

  runtime->EnsureInstalled(
      content::GlobalRenderFrameHostId(
          render_frame_host_->GetProcess()->GetID(),
          render_frame_host_->GetRoutingID()),
      base::BindOnce(&VRServiceImpl::OnInstallResult,
                     weak_ptr_factory_.GetWeakPtr(), std::move(request)));
}

void VRServiceImpl::OnInstallResult(SessionRequestData request,
                                    XrInstallResult result) {
  DVLOG(2) << __func__ << ": result=" << std::to_underlying(result);

  if (result == XrInstallResult::kFailed) {
    RejectSession(std::move(request.callback), request.options->trace_id,
                  device::mojom::RequestSessionError::RUNTIME_INSTALL_FAILURE,
                  "Runtime installation failed.");
    return;
  }

  if (result == XrInstallResult::kSuccessInstalled) {
    // Prolong the user activation since the user may have taken long enough to
    // install the runtime that the transient user activation expired. This is
    // fine to do here, since we enforce that the activation existed prior to
    // kicking off installation.
    DVLOG(3) << __func__ << ": prolonging user activation, current status="
             << render_frame_host_->HasTransientUserActivation();
    render_frame_host_->NotifyUserActivation(
        blink::mojom::UserActivationNotificationType::kInteraction);
  } else {
    DVLOG(3) << __func__
             << ": NOT prolonging user activation (no install UI shown)";
  }

  DoRequestSession(std::move(request));
}

void VRServiceImpl::DoRequestSession(SessionRequestData request) {
  DVLOG(2) << __func__;
  // Get the runtime again, since we're running in an async context
  // and the pointer returned from `GetRuntimeForOptions` is non-owning.
  auto* runtime = runtime_manager_->GetRuntimeForOptions(request.options.get());

  // Ensure that it's the same runtime as the one we expect.
  if (!runtime || runtime->GetId() != request.runtime_id) {
    TRACE_EVENT("xr", "VRServiceImpl::DoRequestSession: mismatching runtime",
                perfetto::Flow::Global(request.options->trace_id));

    RejectSession(std::move(request.callback), request.options->trace_id,
                  device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR,
                  "Mismatching runtime or invalid runtime.");
    return;
  }

  TRACE_EVENT_INSTANT("xr", "GetRuntimeForOptions", "id", request.runtime_id);

  auto runtime_options = GetRuntimeOptions(request.options.get());
  // Make the resolved enabled features available to the runtime.

  runtime_options->required_features.assign(request.required_features.begin(),
                                            request.required_features.end());
  runtime_options->optional_features.assign(request.optional_features.begin(),
                                            request.optional_features.end());
#if BUILDFLAG(IS_ANDROID)
  runtime_options->renderer_information =
      device::mojom::RendererInformation::New(
          ToRendererProcessId(render_frame_host_->GetProcess()->GetID()),
          render_frame_host_->GetRoutingID());
#endif

  if (device::XRSessionModeUtils::IsImmersive(runtime_options->mode)) {
    if (!request.options->tracked_images.empty()) {
      DVLOG(3) << __func__ << ": request.options->tracked_images.size()="
               << request.options->tracked_images.size();
      runtime_options->tracked_images.resize(
          request.options->tracked_images.size());
      for (std::size_t i = 0; i < request.options->tracked_images.size(); ++i) {
        runtime_options->tracked_images[i] =
            request.options->tracked_images[i].Clone();
      }
    }

    runtime_options->depth_options = std::move(request.options->depth_options);

    auto immersive_callback =
        base::BindOnce(&VRServiceImpl::OnImmersiveSessionCreated,
                       weak_ptr_factory_.GetWeakPtr(), std::move(request));

    runtime->RequestImmersiveSession(this, std::move(runtime_options),
                                     std::move(immersive_callback));
  } else {
    auto non_immersive_callback =
        base::BindOnce(&VRServiceImpl::OnInlineSessionCreated,
                       weak_ptr_factory_.GetWeakPtr(), std::move(request));
    runtime->RequestInlineSession(std::move(runtime_options),
                                  std::move(non_immersive_callback));
  }
}

void VRServiceImpl::SupportsSession(
    device::mojom::XRSessionOptionsPtr options,
    device::mojom::VRService::SupportsSessionCallback callback) {
  if (!initialization_complete_) {
    pending_requests_.push_back(
        base::BindOnce(&VRServiceImpl::SupportsSession, base::Unretained(this),
                       std::move(options), std::move(callback)));
    return;
  }

  TRACE_EVENT("xr", "VRServiceImpl::SupportsSession: received",
              perfetto::Flow::Global(options->trace_id));

  if (!IsRenderFrameHostVisible()) {
    bool is_supported_when_hidden = false;
#if BUILDFLAG(IS_ANDROID)
    is_supported_when_hidden = !base::android::device_info::is_desktop() ||
                               base::android::device_info::is_xr();
#endif
    std::move(callback).Run(is_supported_when_hidden);
    return;
  }

  runtime_manager_->SupportsSession(std::move(options), std::move(callback));
}

void VRServiceImpl::ExitPresent(ExitPresentCallback on_exited) {
  BrowserXRRuntimeImpl* immersive_runtime =
      runtime_manager_->GetCurrentlyPresentingImmersiveRuntime();
  DVLOG(2) << __func__ << ": !!immersive_runtime=" << !!immersive_runtime;
  if (immersive_runtime) {
    on_exit_present_ = std::move(on_exited);
    immersive_runtime->ExitPresent(this);
  } else {
    std::move(on_exited).Run();
  }
}

void VRServiceImpl::SetFramesThrottled(bool throttled) {
  if (throttled != frames_throttled_) {
    frames_throttled_ = throttled;
    BrowserXRRuntimeImpl* immersive_runtime =
        runtime_manager_->GetCurrentlyPresentingImmersiveRuntime();
    if (immersive_runtime) {
      immersive_runtime->SetFramesThrottled(this, frames_throttled_);
    }
  }
}

void VRServiceImpl::MakeXrCompatible(
    device::mojom::VRService::MakeXrCompatibleCallback callback) {
  if (!initialization_complete_) {
    pending_requests_.push_back(base::BindOnce(&VRServiceImpl::MakeXrCompatible,
                                               base::Unretained(this),
                                               std::move(callback)));
    return;
  }

  xr_compatible_callbacks_.emplace_back(std::move(callback));

  // Only request compatibility if there aren't any pending calls.
  // OnMakeXrCompatibleComplete will run all callbacks.
  if (xr_compatible_callbacks_.size() == 1)
    runtime_manager_->MakeXrCompatible();
}

void VRServiceImpl::OnMakeXrCompatibleComplete(
    device::mojom::XrCompatibleResult result) {
  for (XrCompatibleCallback& wrapper : xr_compatible_callbacks_)
    std::move(wrapper.callback).Run(result);

  xr_compatible_callbacks_.clear();
}

void VRServiceImpl::OnExitPresent() {
  DVLOG(2) << __func__;

  if (render_frame_host_) {
    // Clear any XrRenderTarget that may have been set.
    viz::FrameSinkId default_frame_sink_id;
    static_cast<WebContentsImpl*>(GetWebContents())
        ->OnXrHasRenderTarget(default_frame_sink_id);

    if (has_immersive_session_) {
      render_frame_host_->GetProcess()->OnImmersiveXrSessionStopped();
      GetSessionMetricsHelper()->StopAndRecordImmersiveSession();
      has_immersive_session_ = false;
    }
  }

  if (on_exit_present_) {
    std::move(on_exit_present_).Run();
  }

  for (auto& client : session_clients_) {
    // https://crbug.com/1160940 has a fairly generic callstack, in mojom
    // generated code, which appears to aggregate a few different actual crashes
    // into the same bug. For the crashes that appear to be our fault, the
    // common "start" is this call. By causing a CHECK here instead of in the
    // mojom generated code, we can isolate our crashes.
    CHECK(client);
    client->OnExitPresent();
  }

  // Ensure that the client list is erased to avoid "Cannot issue Interface
  // method calls on an unbound Remote" errors: https://crbug.com/991747
  session_clients_.Clear();
}

void VRServiceImpl::OnVisibilityStateChanged(
    device::mojom::XRVisibilityState visiblity_state) {
  visibility_state_ = visiblity_state;
  for (auto& client : session_clients_)
    client->OnVisibilityStateChanged(visiblity_state);
}

content::WebContents* VRServiceImpl::GetWebContents() {
  return content::WebContents::FromRenderFrameHost(render_frame_host_);
}

bool VRServiceImpl::IsRenderFrameHostVisible() const {
  return render_frame_host_ && render_frame_host_->GetVisibilityState() ==
                                   content::PageVisibilityState::kVisible;
}

void VRServiceImpl::Teardown() {
  if (!render_frame_host_) {
    return;
  }

  for (auto it = magic_window_controllers_.begin();
       it != magic_window_controllers_.end(); ++it) {
    OnInlineSessionDisconnected(it.id());
  }
  magic_window_controllers_.Clear();

  OnExitPresent();

  runtime_manager_->RemoveService(this);
  render_frame_host_ = nullptr;
}

}  // namespace content
