blob: 6f74d32477e230f48c1c10ac1b832d79c302cf4b [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/vr/service/vr_service_impl.h"
#include <utility>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/metrics/histogram_macros.h"
#include "base/trace_event/common/trace_event_common.h"
#include "build/build_config.h"
#include "chrome/browser/vr/metrics/session_metrics_helper.h"
#include "chrome/browser/vr/mode.h"
#include "chrome/browser/vr/service/browser_xr_runtime.h"
#include "chrome/browser/vr/service/xr_runtime_manager.h"
#include "chrome/common/chrome_switches.h"
#include "components/ukm/content/source_url_recorder.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/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/origin_util.h"
#include "device/vr/buildflags/buildflags.h"
#include "device/vr/vr_device.h"
#if defined(OS_WIN)
#include "chrome/browser/vr/service/xr_session_request_consent_manager.h"
#elif defined(OS_ANDROID)
#include "chrome/browser/vr/service/gvr_consent_helper.h"
#if BUILDFLAG(ENABLE_ARCORE)
#include "chrome/browser/vr/service/arcore_consent_prompt_interface.h"
#endif
#endif
namespace {
// TODO(https://crbug.com/960132): When we unship WebVR 1.1, set this to false.
static constexpr bool kAllowHTTPWebVRWithFlag = true;
bool IsSecureContext(content::RenderFrameHost* host) {
if (!host)
return false;
while (host != nullptr) {
if (!content::IsOriginSecure(host->GetLastCommittedURL()))
return false;
host = host->GetParent();
}
return true;
}
device::mojom::XRRuntimeSessionOptionsPtr GetRuntimeOptions(
device::mojom::XRSessionOptions* options) {
device::mojom::XRRuntimeSessionOptionsPtr runtime_options =
device::mojom::XRRuntimeSessionOptions::New();
runtime_options->immersive = options->immersive;
runtime_options->environment_integration = options->environment_integration;
runtime_options->is_legacy_webvr = options->is_legacy_webvr;
return runtime_options;
}
vr::XrConsentPromptLevel GetRequiredConsentLevel(
bool immersive,
const vr::BrowserXRRuntime* runtime,
const std::set<device::mojom::XRSessionFeature>& requested_features) {
if (requested_features.find(
device::mojom::XRSessionFeature::REF_SPACE_BOUNDED_FLOOR) !=
requested_features.end()) {
return vr::XrConsentPromptLevel::kVRFloorPlan;
}
// If the device supports a custom IPD and it will be exposed (via immersive),
// we need to warn about physical features Being exposed.
if (runtime->SupportsCustomIPD() && immersive) {
return vr::XrConsentPromptLevel::kVRFeatures;
}
// If local-floor is requested and the device supports a user inputted or real
// height, we need to warn about physical features being exposed.
// Note that while this is also the case for bounded-floor, that is covered
// by the stricter kVRFloorPlan Prompt set above.
if (requested_features.find(
device::mojom::XRSessionFeature::REF_SPACE_LOCAL_FLOOR) !=
requested_features.end() &&
runtime->SupportsNonEmulatedHeight()) {
return vr::XrConsentPromptLevel::kVRFeatures;
}
// In the absence of other items that need to be consented, an immersive
// session always requires some level of consent.
if (immersive) {
return vr::XrConsentPromptLevel::kDefault;
}
return vr::XrConsentPromptLevel::kNone;
}
} // namespace
namespace vr {
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_);
runtime_manager_ = XRRuntimeManager::GetOrCreateInstance();
runtime_manager_->AddService(this);
magic_window_controllers_.set_connection_error_handler(base::BindRepeating(
&VRServiceImpl::OnInlineSessionDisconnected,
base::Unretained(this))); // Unretained is OK since the collection is
// owned by VRServiceImpl.
}
// Constructor for testing.
VRServiceImpl::VRServiceImpl(util::PassKey<XRRuntimeManagerTest>)
: render_frame_host_(nullptr) {
runtime_manager_ = XRRuntimeManager::GetOrCreateInstance();
runtime_manager_->AddService(this);
}
VRServiceImpl::~VRServiceImpl() {
runtime_manager_->RemoveService(this);
}
void VRServiceImpl::Create(
content::RenderFrameHost* render_frame_host,
mojo::PendingReceiver<device::mojom::VRService> receiver) {
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.
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;
}
service_client_.Bind(std::move(service_client));
}
void VRServiceImpl::ResolvePendingRequests() {
for (auto& callback : pending_requests_) {
std::move(callback).Run();
}
pending_requests_.clear();
}
void VRServiceImpl::OnDisplayInfoChanged() {
device::mojom::VRDisplayInfoPtr display_info =
runtime_manager_->GetCurrentVRDisplayInfo(this);
if (display_info) {
for (auto& client : session_clients_)
client->OnChanged(display_info.Clone());
}
}
void VRServiceImpl::RuntimesChanged() {
OnDisplayInfoChanged();
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) {
if (host != render_frame_host_)
return;
// 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::OnWebContentsFocusChanged(content::RenderWidgetHost* host,
bool focused) {
if (!render_frame_host_->GetView() ||
render_frame_host_->GetView()->GetRenderWidgetHost() != host) {
return;
}
in_focused_frame_ = focused;
if (ListeningForActivate()) {
BrowserXRRuntime* immersive_runtime =
runtime_manager_->GetImmersiveRuntime();
if (immersive_runtime)
immersive_runtime->UpdateListeningForActivate(this);
}
magic_window_controllers_.ForAllPtrs(
[focused](device::mojom::XRSessionController* controller) {
controller->SetFrameDataRestricted(!focused);
});
}
// static
bool VRServiceImpl::IsXrDeviceConsentPromptDisabledForTesting() {
static bool is_xr_device_consent_prompt_disabled_for_testing =
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kDisableXrDeviceConsentPromptForTesting);
return is_xr_device_consent_prompt_disabled_for_testing;
}
void VRServiceImpl::OnInlineSessionCreated(
device::mojom::XRDeviceId session_runtime_id,
device::mojom::VRService::RequestSessionCallback callback,
const std::set<device::mojom::XRSessionFeature>& enabled_features,
device::mojom::XRSessionPtr session,
device::mojom::XRSessionControllerPtr controller) {
if (!session) {
std::move(callback).Run(
device::mojom::RequestSessionResult::NewFailureReason(
device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR));
return;
}
// Start giving out magic window data if we are focused.
controller->SetFrameDataRestricted(!in_focused_frame_);
auto id = magic_window_controllers_.AddPtr(std::move(controller));
// Note: We might be recording an inline session that was created by WebVR.
GetSessionMetricsHelper()->RecordInlineSessionStart(id);
OnSessionCreated(session_runtime_id, std::move(callback), enabled_features,
std::move(session));
}
void VRServiceImpl::OnInlineSessionDisconnected(size_t session_id) {
// Notify metrics helper that inline session was stopped.
auto* metrics_helper = GetSessionMetricsHelper();
metrics_helper->RecordInlineSessionStop(session_id);
}
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, Mode::kNoVr);
}
return metrics_helper;
}
void VRServiceImpl::OnSessionCreated(
device::mojom::XRDeviceId session_runtime_id,
device::mojom::VRService::RequestSessionCallback callback,
const std::set<device::mojom::XRSessionFeature>& enabled_features,
device::mojom::XRSessionPtr session) {
if (!session) {
std::move(callback).Run(
device::mojom::RequestSessionResult::NewFailureReason(
device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR));
return;
}
UMA_HISTOGRAM_ENUMERATION("XR.RuntimeUsed", session_runtime_id);
mojo::Remote<device::mojom::XRSessionClient> client;
session->client_receiver = client.BindNewPipeAndPassReceiver();
session->enabled_features.clear();
for (const auto& feature : enabled_features) {
session->enabled_features.push_back(feature);
}
client->OnVisibilityStateChanged(visibility_state_);
session_clients_.Add(std::move(client));
std::move(callback).Run(
device::mojom::RequestSessionResult::NewSession(std::move(session)));
}
void VRServiceImpl::RequestSession(
device::mojom::XRSessionOptionsPtr options,
device::mojom::VRService::RequestSessionCallback callback) {
DCHECK(options);
// Queue the request to get to when initialization has completed.
if (!initialization_complete_) {
pending_requests_.push_back(
base::BindOnce(&VRServiceImpl::RequestSession, base::Unretained(this),
std::move(options), std::move(callback)));
return;
}
// Check that the request satisfies secure context requirements.
if (!IsSecureContextRequirementSatisfied()) {
std::move(callback).Run(
device::mojom::RequestSessionResult::NewFailureReason(
device::mojom::RequestSessionError::ORIGIN_NOT_SECURE));
return;
}
if (runtime_manager_->IsOtherClientPresenting(this)) {
// Can't create sessions while an immersive session exists.
std::move(callback).Run(
device::mojom::RequestSessionResult::NewFailureReason(
device::mojom::RequestSessionError::EXISTING_IMMERSIVE_SESSION));
return;
}
auto* runtime = runtime_manager_->GetRuntimeForOptions(options.get());
if (!runtime) {
std::move(callback).Run(
device::mojom::RequestSessionResult::NewFailureReason(
device::mojom::RequestSessionError::NO_RUNTIME_FOUND));
return;
}
// GetRuntimeForOptions should only return a device that supports all required
// features.
std::set<device::mojom::XRSessionFeature> requested_features;
for (const auto& feature : options->required_features) {
requested_features.insert(feature);
}
// 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. Add all requested features to the set of supported features.
for (const auto& feature : options->optional_features) {
if (runtime->SupportsFeature(feature)) {
requested_features.insert(feature);
}
}
ShowConsentPrompt(std::move(options), std::move(callback), runtime,
std::move(requested_features));
}
void VRServiceImpl::ShowConsentPrompt(
device::mojom::XRSessionOptionsPtr options,
device::mojom::VRService::RequestSessionCallback callback,
BrowserXRRuntime* runtime,
std::set<device::mojom::XRSessionFeature> requested_features) {
DCHECK(options);
DCHECK(runtime);
#if defined(OS_WIN)
DCHECK(!options->environment_integration);
#endif
// WebVR did not require permissions.
// TODO(crbug.com/968221): Address privacy requirements for inline sessions
if (options->is_legacy_webvr) {
DoRequestSession(std::move(options), std::move(callback), runtime,
std::move(requested_features));
return;
}
XrConsentPromptLevel consent_level =
GetRequiredConsentLevel(options->immersive, runtime, requested_features);
// Skip the consent prompt if the user has already consented for this device,
// or if consent is not needed.
if (consent_level == XrConsentPromptLevel::kNone ||
IsConsentGrantedForDevice(runtime->GetId(), consent_level) ||
IsXrDeviceConsentPromptDisabledForTesting()) {
DoRequestSession(std::move(options), std::move(callback), runtime,
std::move(requested_features));
return;
}
// TODO(crbug.com/968233): Unify the below consent flow.
#if defined(OS_ANDROID)
if (options->environment_integration) {
#if BUILDFLAG(ENABLE_ARCORE)
ArCoreConsentPromptInterface::GetInstance()->ShowConsentPrompt(
render_frame_host_->GetProcess()->GetID(),
render_frame_host_->GetRoutingID(),
base::BindOnce(&VRServiceImpl::OnConsentResult,
weak_ptr_factory_.GetWeakPtr(), std::move(options),
std::move(callback), runtime->GetId(),
std::move(requested_features), consent_level));
return;
#else
std::move(callback).Run(nullptr);
return;
#endif
} else {
// GVR.
GvrConsentHelper::GetInstance()->PromptUserAndGetConsent(
render_frame_host_->GetProcess()->GetID(),
render_frame_host_->GetRoutingID(), consent_level,
base::BindOnce(&VRServiceImpl::OnConsentResult,
weak_ptr_factory_.GetWeakPtr(), std::move(options),
std::move(callback), runtime->GetId(),
std::move(requested_features)));
return;
}
#elif defined(OS_WIN)
XRSessionRequestConsentManager::Instance()->ShowDialogAndGetConsent(
GetWebContents(), consent_level,
base::BindOnce(&VRServiceImpl::OnConsentResult,
weak_ptr_factory_.GetWeakPtr(), std::move(options),
std::move(callback), runtime->GetId(),
std::move(requested_features)));
return;
#endif
NOTREACHED();
}
void VRServiceImpl::OnConsentResult(
device::mojom::XRSessionOptionsPtr options,
device::mojom::VRService::RequestSessionCallback callback,
device::mojom::XRDeviceId expected_runtime_id,
std::set<device::mojom::XRSessionFeature> enabled_features,
XrConsentPromptLevel consent_level,
bool is_consent_granted) {
if (!is_consent_granted) {
std::move(callback).Run(
device::mojom::RequestSessionResult::NewFailureReason(
device::mojom::RequestSessionError::USER_DENIED_CONSENT));
return;
}
// 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(options.get());
// Ensure that it's the same runtime as the one we expect.
if (!runtime || runtime->GetId() != expected_runtime_id) {
std::move(callback).Run(
device::mojom::RequestSessionResult::NewFailureReason(
device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR));
return;
}
AddConsentGrantedDevice(runtime->GetId(), consent_level);
// 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.
std::move(callback).Run(
device::mojom::RequestSessionResult::NewFailureReason(
device::mojom::RequestSessionError::EXISTING_IMMERSIVE_SESSION));
return;
}
DoRequestSession(std::move(options), std::move(callback), runtime,
std::move(enabled_features));
}
void VRServiceImpl::DoRequestSession(
device::mojom::XRSessionOptionsPtr options,
device::mojom::VRService::RequestSessionCallback callback,
BrowserXRRuntime* runtime,
std::set<device::mojom::XRSessionFeature> enabled_features) {
// Get the runtime we'll be creating a session with.
DCHECK(runtime);
device::mojom::XRDeviceId session_runtime_id = runtime->GetId();
TRACE_EVENT_INSTANT1("xr", "GetRuntimeForOptions", TRACE_EVENT_SCOPE_THREAD,
"id", session_runtime_id);
auto runtime_options = GetRuntimeOptions(options.get());
#if defined(OS_ANDROID) && BUILDFLAG(ENABLE_ARCORE)
if (session_runtime_id == device::mojom::XRDeviceId::ARCORE_DEVICE_ID) {
runtime_options->render_process_id =
render_frame_host_->GetProcess()->GetID();
runtime_options->render_frame_id = render_frame_host_->GetRoutingID();
}
#endif
// Make the resolved enabled features available to the runtime.
runtime_options->enabled_features.reserve(enabled_features.size());
for (const auto& feature : enabled_features) {
runtime_options->enabled_features.push_back(feature);
}
if (runtime_options->immersive) {
GetSessionMetricsHelper()->ReportRequestPresent(*runtime_options);
base::OnceCallback<void(device::mojom::XRSessionPtr)> immersive_callback =
base::BindOnce(&VRServiceImpl::OnSessionCreated,
weak_ptr_factory_.GetWeakPtr(), session_runtime_id,
std::move(callback), std::move(enabled_features));
runtime->RequestSession(this, std::move(runtime_options),
std::move(immersive_callback));
} else {
base::OnceCallback<void(device::mojom::XRSessionPtr,
device::mojom::XRSessionControllerPtr)>
non_immersive_callback =
base::BindOnce(&VRServiceImpl::OnInlineSessionCreated,
weak_ptr_factory_.GetWeakPtr(), session_runtime_id,
std::move(callback), std::move(enabled_features));
runtime->GetRuntime()->RequestSession(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;
}
runtime_manager_->SupportsSession(std::move(options), std::move(callback));
}
void VRServiceImpl::ExitPresent() {
BrowserXRRuntime* immersive_runtime = runtime_manager_->GetImmersiveRuntime();
if (immersive_runtime)
immersive_runtime->ExitPresent(this);
}
void VRServiceImpl::SetListeningForActivate(
mojo::PendingRemote<device::mojom::VRDisplayClient> display_client) {
// TODO(crbug.com/999745): Remove the check if the condition to check if
// |display_client| is nullptr is not required.
if (display_client)
display_client_.Bind(std::move(display_client));
else
display_client_.reset();
BrowserXRRuntime* immersive_runtime = runtime_manager_->GetImmersiveRuntime();
if (immersive_runtime && display_client_) {
immersive_runtime->UpdateListeningForActivate(this);
}
}
void VRServiceImpl::GetImmersiveVRDisplayInfo(
device::mojom::VRService::GetImmersiveVRDisplayInfoCallback callback) {
if (!initialization_complete_) {
pending_requests_.push_back(
base::BindOnce(&VRServiceImpl::GetImmersiveVRDisplayInfo,
base::Unretained(this), std::move(callback)));
return;
}
BrowserXRRuntime* immersive_runtime = runtime_manager_->GetImmersiveRuntime();
if (immersive_runtime) {
immersive_runtime->InitializeAndGetDisplayInfo(render_frame_host_,
std::move(callback));
return;
}
std::move(callback).Run(nullptr);
}
void VRServiceImpl::OnExitPresent() {
for (auto& client : session_clients_)
client->OnExitPresent();
}
void VRServiceImpl::OnVisibilityStateChanged(
device::mojom::XRVisibilityState visiblity_state) {
visibility_state_ = visiblity_state;
for (auto& client : session_clients_)
client->OnVisibilityStateChanged(visiblity_state);
}
void VRServiceImpl::OnActivate(device::mojom::VRDisplayEventReason reason,
base::OnceCallback<void(bool)> on_handled) {
if (display_client_) {
display_client_->OnActivate(reason, std::move(on_handled));
}
}
void VRServiceImpl::OnDeactivate(device::mojom::VRDisplayEventReason reason) {
if (display_client_) {
display_client_->OnDeactivate(reason);
}
}
content::WebContents* VRServiceImpl::GetWebContents() {
return content::WebContents::FromRenderFrameHost(render_frame_host_);
}
bool VRServiceImpl::IsSecureContextRequirementSatisfied() {
// We require secure connections unless both the webvr flag and the
// http flag are enabled.
static bool requires_secure_context =
!kAllowHTTPWebVRWithFlag ||
!base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kEnableWebVR);
if (!requires_secure_context)
return true;
return IsSecureContext(render_frame_host_);
}
bool VRServiceImpl::IsConsentGrantedForDevice(
device::mojom::XRDeviceId device_id,
XrConsentPromptLevel consent_level) {
auto it = consent_granted_devices_.find(device_id);
return it != consent_granted_devices_.end() && it->second >= consent_level;
}
void VRServiceImpl::AddConsentGrantedDevice(
device::mojom::XRDeviceId device_id,
XrConsentPromptLevel consent_level) {
auto it = consent_granted_devices_.find(device_id);
if (it == consent_granted_devices_.end() || it->second < consent_level) {
consent_granted_devices_[device_id] = consent_level;
}
}
} // namespace vr