blob: 652c918a502df7a9b31716a2a77302a07bee4b51 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/vr/ui_host/vr_ui_host_impl.h"
#include <memory>
#include "base/containers/contains.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/media/webrtc/media_stream_capture_indicator.h"
#include "chrome/browser/vr/vr_browser_renderer_thread.h"
#include "chrome/browser/vr/vr_tab_helper.h"
#include "components/content_settings/browser/page_specific_content_settings.h"
#include "components/permissions/permission_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/device_service.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/permission_controller.h"
#include "content/public/browser/permission_descriptor_util.h"
#include "content/public/browser/xr_runtime_manager.h"
#include "device/base/features.h"
#include "device/vr/public/mojom/vr_service.mojom.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "ui/base/l10n/l10n_util.h"
namespace vr {
namespace {
static constexpr base::TimeDelta kPermissionPromptTimeout = base::Seconds(5);
#if BUILDFLAG(IS_WIN)
// Some runtimes on Windows have quite lengthy lengthy startup animations that
// may cause indicators/permissions to not be visible during the normal timeout.
static constexpr base::TimeDelta kFirstWindowsPermissionPromptTimeout =
base::Seconds(10);
#endif
base::TimeDelta GetPermissionPromptTimeout(bool first_time) {
#if BUILDFLAG(IS_WIN)
if (first_time)
return kFirstWindowsPermissionPromptTimeout;
#endif
return kPermissionPromptTimeout;
}
static constexpr base::TimeDelta kPollCapturingStateInterval =
base::Seconds(0.2);
const CapturingStateModel g_default_capturing_state;
} // namespace
VRUiHostImpl::CapturingStateModelTransience::CapturingStateModelTransience(
CapturingStateModel* capturing_model)
: active_capture_state_model_(capturing_model) {}
void VRUiHostImpl::CapturingStateModelTransience::ResetStartTimes() {
auto now = base::Time::Now();
midi_indicator_start_ = now;
usb_indicator_start_ = now;
bluetooth_indicator_start_ = now;
location_indicator_start_ = now;
screen_capture_indicator_start_ = now;
video_indicator_start_ = now;
audio_indicator_start_ = now;
}
void VRUiHostImpl::CapturingStateModelTransience::
TurnFlagsOnBasedOnTriggeredState(
const CapturingStateModel& model_with_triggered_states) {
auto now = base::Time::Now();
if (model_with_triggered_states.audio_capture_enabled) {
audio_indicator_start_ = now;
active_capture_state_model_->audio_capture_enabled = true;
}
if (model_with_triggered_states.video_capture_enabled) {
video_indicator_start_ = now;
active_capture_state_model_->video_capture_enabled = true;
}
if (model_with_triggered_states.screen_capture_enabled) {
screen_capture_indicator_start_ = now;
active_capture_state_model_->screen_capture_enabled = true;
}
if (model_with_triggered_states.location_access_enabled) {
location_indicator_start_ = now;
active_capture_state_model_->location_access_enabled = true;
}
if (model_with_triggered_states.bluetooth_connected) {
bluetooth_indicator_start_ = now;
active_capture_state_model_->bluetooth_connected = true;
}
if (model_with_triggered_states.usb_connected) {
usb_indicator_start_ = now;
active_capture_state_model_->usb_connected = true;
}
if (model_with_triggered_states.midi_connected) {
midi_indicator_start_ = now;
active_capture_state_model_->midi_connected = true;
}
}
void VRUiHostImpl::CapturingStateModelTransience::
TurnOffAllFlagsTogetherWhenAllTransiencesExpire(
const base::TimeDelta& transience_period) {
if (!active_capture_state_model_->IsAtleastOnePermissionGrantedOrInUse())
return;
auto now = base::Time::Now();
if ((!active_capture_state_model_->audio_capture_enabled ||
now > audio_indicator_start_ + transience_period) &&
(!active_capture_state_model_->video_capture_enabled ||
now > video_indicator_start_ + transience_period) &&
(!active_capture_state_model_->screen_capture_enabled ||
now > screen_capture_indicator_start_ + transience_period) &&
(!active_capture_state_model_->location_access_enabled ||
now > location_indicator_start_ + transience_period) &&
(!active_capture_state_model_->bluetooth_connected ||
now > bluetooth_indicator_start_ + transience_period) &&
(!active_capture_state_model_->usb_connected ||
now > usb_indicator_start_ + transience_period) &&
(!active_capture_state_model_->midi_connected ||
now > midi_indicator_start_ + transience_period))
*active_capture_state_model_ = CapturingStateModel();
}
VRUiHostImpl::VRUiHostImpl(
content::WebContents& contents,
const std::vector<device::mojom::XRViewPtr>& views,
mojo::PendingRemote<device::mojom::ImmersiveOverlay> overlay)
: web_contents_(contents.GetWeakPtr()),
main_thread_task_runner_(
base::SingleThreadTaskRunner::GetCurrentDefault()),
triggered_capturing_transience_(&triggered_capturing_state_model_) {
DVLOG(1) << __func__;
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
CHECK(base::Contains(views, device::mojom::XREye::kLeft,
&device::mojom::XRView::eye));
CHECK(base::Contains(views, device::mojom::XREye::kRight,
&device::mojom::XRView::eye));
DesktopMediaPickerManager::Get()->AddObserver(this);
VrTabHelper::SetIsContentDisplayedInHeadset(&contents, true);
ui_rendering_thread_ =
std::make_unique<VRBrowserRendererThread>(std::move(overlay), views);
InitCapturingStates();
PollCapturingState();
permissions::PermissionRequestManager::CreateForWebContents(&contents);
raw_ptr<permissions::PermissionRequestManager> permission_request_manager =
permissions::PermissionRequestManager::FromWebContents(&contents);
// Attaching a permission request manager to WebContents can fail, so a
// DCHECK would be inappropriate here. If it fails, the user won't get
// notified about permission prompts in the headset, but other than that the
// session would work normally.
if (permission_request_manager) {
permission_request_manager->AddObserver(this);
// There might already be a visible permission bubble from before
// we registered the observer, show the HMD message now in that case.
if (permission_request_manager->IsRequestInProgress()) {
OnPromptAdded();
}
}
}
VRUiHostImpl::~VRUiHostImpl() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DVLOG(1) << __func__;
// Ensure that we unregister pre-existing observers, if any.
// It's safe to try to remove a nonexistent observer.
DesktopMediaPickerManager::Get()->RemoveObserver(this);
poll_capturing_state_task_.Cancel();
ui_rendering_thread_.reset();
// Even though we think we should have a WebContents, we only hold onto it
// as a WeakPtr, so it may have been destroyed. So check here before we do
// any cleanup.
if (!web_contents_) {
return;
}
VrTabHelper::SetIsContentDisplayedInHeadset(web_contents_.get(), false);
raw_ptr<permissions::PermissionRequestManager> permission_manager =
permissions::PermissionRequestManager::FromWebContents(
web_contents_.get());
if (permission_manager) {
permission_manager->RemoveObserver(this);
}
}
void VRUiHostImpl::WebXRFramesThrottledChanged(bool throttled) {
ui_rendering_thread_->SetFramesThrottled(throttled);
}
void VRUiHostImpl::OnPromptAdded() {
ShowExternalNotificationPrompt();
}
void VRUiHostImpl::OnPromptRemoved() {
RemoveHeadsetNotificationPrompt();
}
void VRUiHostImpl::OnDialogOpened(const DesktopMediaPicker::Params&) {
ShowExternalNotificationPrompt();
}
void VRUiHostImpl::OnDialogClosed() {
RemoveHeadsetNotificationPrompt();
}
void VRUiHostImpl::ShowExternalNotificationPrompt() {
if (indicators_visible_) {
indicators_visible_ = false;
ui_rendering_thread_->SetIndicatorsVisible(false);
}
ui_rendering_thread_->SetVisibleExternalPromptNotification(
ExternalPromptNotificationType::kPromptGenericPermission);
is_external_prompt_showing_in_headset_ = true;
external_prompt_timeout_task_.Reset(
base::BindOnce(&VRUiHostImpl::RemoveHeadsetNotificationPrompt,
weak_ptr_factory_.GetWeakPtr()));
main_thread_task_runner_->PostDelayedTask(
FROM_HERE, external_prompt_timeout_task_.callback(),
kPermissionPromptTimeout);
}
void VRUiHostImpl::RemoveHeadsetNotificationPrompt() {
if (!external_prompt_timeout_task_.IsCancelled())
external_prompt_timeout_task_.Cancel();
if (!is_external_prompt_showing_in_headset_)
return;
is_external_prompt_showing_in_headset_ = false;
ui_rendering_thread_->SetVisibleExternalPromptNotification(
ExternalPromptNotificationType::kPromptNone);
indicators_shown_start_time_ = base::Time::Now();
}
void VRUiHostImpl::InitCapturingStates() {
active_capturing_ = g_default_capturing_state;
potential_capturing_ = g_default_capturing_state;
CHECK(web_contents_);
content::PermissionController* permission_controller =
web_contents_->GetBrowserContext()->GetPermissionController();
potential_capturing_.audio_capture_enabled =
permission_controller->GetPermissionStatusForCurrentDocument(
content::PermissionDescriptorUtil::
CreatePermissionDescriptorForPermissionType(
blink::PermissionType::AUDIO_CAPTURE),
web_contents_->GetPrimaryMainFrame()) ==
blink::mojom::PermissionStatus::GRANTED;
potential_capturing_.video_capture_enabled =
permission_controller->GetPermissionStatusForCurrentDocument(
content::PermissionDescriptorUtil::
CreatePermissionDescriptorForPermissionType(
blink::PermissionType::VIDEO_CAPTURE),
web_contents_->GetPrimaryMainFrame()) ==
blink::mojom::PermissionStatus::GRANTED;
potential_capturing_.location_access_enabled =
permission_controller->GetPermissionStatusForCurrentDocument(
content::PermissionDescriptorUtil::
CreatePermissionDescriptorForPermissionType(
blink::PermissionType::GEOLOCATION),
web_contents_->GetPrimaryMainFrame()) ==
blink::mojom::PermissionStatus::GRANTED;
potential_capturing_.midi_connected =
permission_controller->GetPermissionStatusForCurrentDocument(
content::PermissionDescriptorUtil::
CreatePermissionDescriptorForPermissionType(
blink::PermissionType::MIDI_SYSEX),
web_contents_->GetPrimaryMainFrame()) ==
blink::mojom::PermissionStatus::GRANTED;
indicators_shown_start_time_ = base::Time::Now();
indicators_visible_ = false;
indicators_showing_first_time_ = true;
triggered_capturing_transience_.ResetStartTimes();
}
void VRUiHostImpl::PollCapturingState() {
poll_capturing_state_task_.Reset(base::BindOnce(
&VRUiHostImpl::PollCapturingState, base::Unretained(this)));
main_thread_task_runner_->PostDelayedTask(
FROM_HERE, poll_capturing_state_task_.callback(),
kPollCapturingStateInterval);
// location, microphone, camera, midi.
CapturingStateModel active_capturing = active_capturing_;
// TODO(crbug.com/40139135): Plumb the actual frame reference here (we
// should get a RFH from VRServiceImpl instead of WebContents)
if (web_contents_) {
content_settings::PageSpecificContentSettings* settings =
content_settings::PageSpecificContentSettings::GetForFrame(
web_contents_->GetPrimaryMainFrame());
if (settings) {
active_capturing.location_access_enabled = settings->IsContentAllowed(
permissions::PermissionUtil::GetGeolocationType());
active_capturing.audio_capture_enabled =
settings->GetMicrophoneCameraState().Has(
content_settings::PageSpecificContentSettings::
kMicrophoneAccessed) &&
!settings->GetMicrophoneCameraState().Has(
content_settings::PageSpecificContentSettings::
kMicrophoneBlocked);
active_capturing.video_capture_enabled =
settings->GetMicrophoneCameraState().Has(
content_settings::PageSpecificContentSettings::kCameraAccessed) &&
!settings->GetMicrophoneCameraState().Has(
content_settings::PageSpecificContentSettings::kCameraBlocked);
active_capturing.midi_connected =
settings->IsContentAllowed(ContentSettingsType::MIDI_SYSEX);
}
// Screen capture.
scoped_refptr<MediaStreamCaptureIndicator> indicator =
MediaCaptureDevicesDispatcher::GetInstance()
->GetMediaStreamCaptureIndicator();
active_capturing.screen_capture_enabled =
indicator->IsBeingMirrored(web_contents_.get()) ||
indicator->IsCapturingWindow(web_contents_.get()) ||
indicator->IsCapturingDisplay(web_contents_.get());
// Bluetooth.
active_capturing.bluetooth_connected = web_contents_->IsCapabilityActive(
content::WebContentsCapabilityType::kBluetoothConnected);
// USB.
active_capturing.usb_connected = web_contents_->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB);
}
auto capturing_switched_on =
active_capturing.NewlyUpdatedPermissions(active_capturing_);
if (capturing_switched_on.IsAtleastOnePermissionGrantedOrInUse()) {
indicators_shown_start_time_ = base::Time::Now();
triggered_capturing_transience_.TurnFlagsOnBasedOnTriggeredState(
capturing_switched_on);
active_capturing_ = active_capturing;
}
triggered_capturing_transience_
.TurnOffAllFlagsTogetherWhenAllTransiencesExpire(
GetPermissionPromptTimeout(indicators_showing_first_time_));
ui_rendering_thread_->SetCapturingState(triggered_capturing_state_model_,
g_default_capturing_state,
potential_capturing_);
if (indicators_shown_start_time_ +
GetPermissionPromptTimeout(indicators_showing_first_time_) >
base::Time::Now()) {
if (!indicators_visible_ && !is_external_prompt_showing_in_headset_) {
indicators_visible_ = true;
ui_rendering_thread_->SetIndicatorsVisible(true);
}
} else {
indicators_showing_first_time_ = false;
potential_capturing_ = CapturingStateModel();
if (indicators_visible_) {
indicators_visible_ = false;
ui_rendering_thread_->SetIndicatorsVisible(false);
}
}
}
} // namespace vr