blob: 6d0128ea3c80845a3ebcacc271608fc1efaae753 [file] [log] [blame]
// Copyright 2025 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/ui/tabs/alert/tab_alert_controller.h"
#include <functional>
#include <optional>
#include <vector>
#include "base/check.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/flat_set.h"
#include "base/containers/to_vector.h"
#include "base/functional/bind.h"
#include "chrome/browser/actor/ui/actor_ui_tab_controller.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/media/webrtc/media_stream_capture_indicator.h"
#include "chrome/browser/ui/recently_audible_helper.h"
#include "chrome/browser/ui/tabs/alert/tab_alert.h"
#include "chrome/browser/vr/vr_tab_helper.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_capability_type.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/unowned_user_data/scoped_unowned_user_data.h"
#if BUILDFLAG(ENABLE_GLIC)
#include "chrome/browser/glic/browser_ui/glic_tab_indicator_helper.h"
#include "chrome/browser/glic/public/context/glic_sharing_manager.h"
#include "chrome/browser/glic/public/glic_keyed_service.h"
#endif // BUILDFLAG(ENABLE_GLIC)
namespace tabs {
DEFINE_USER_DATA(TabAlertController);
bool CompareAlerts::operator()(TabAlert first, TabAlert second) const {
// Alerts are ordered from highest priority to be shown to lowest priority.
static constexpr auto tab_alert_priority =
base::MakeFixedFlatMap<TabAlert, int>(
{{TabAlert::kDesktopCapturing, 17},
{TabAlert::kTabCapturing, 16},
{TabAlert::kMediaRecording, 15},
{TabAlert::kAudioRecording, 14},
{TabAlert::kVideoRecording, 13},
{TabAlert::kBluetoothConnected, 12},
{TabAlert::kBluetoothScanActive, 11},
{TabAlert::kUsbConnected, 10},
{TabAlert::kHidConnected, 9},
{TabAlert::kSerialConnected, 8},
{TabAlert::kActorWaitingOnUser, 7},
{TabAlert::kActorAccessing, 6},
{TabAlert::kGlicAccessing, 5},
{TabAlert::kGlicSharing, 4},
// NOTE: VR must take priority over the audio alert ones
// because most VR content has audio and its usage is implied by the
// VR icon.
{TabAlert::kVrPresentingInHeadset, 3},
{TabAlert::kPipPlaying, 2},
{TabAlert::kAudioMuting, 1},
{TabAlert::kAudioPlaying, 0}});
return tab_alert_priority.at(first) > tab_alert_priority.at(second);
}
TabAlertController::TabAlertController(TabInterface& tab)
: tabs::ContentsObservingTabFeature(tab),
scoped_unowned_user_data_(tab.GetUnownedUserDataHost(), *this) {
media_stream_capture_indicator_observation_.Observe(
MediaCaptureDevicesDispatcher::GetInstance()
->GetMediaStreamCaptureIndicator()
.get());
vr_tab_helper_observation_.Observe(
vr::VrTabHelper::FromWebContents(web_contents()));
recently_audible_subscription_ =
RecentlyAudibleHelper::FromWebContents(tab.GetContents())
->RegisterRecentlyAudibleChangedCallback(base::BindRepeating(
&TabAlertController::OnRecentlyAudibleStateChanged,
base::Unretained(this)));
if (auto* actor_ui_tab_controller =
actor::ui::ActorUiTabController::From(&tab)) {
actor_tab_indicator_callback_runner_ =
actor_ui_tab_controller->RegisterActorTabIndicatorStateChangedCallback(
base::BindRepeating(
&TabAlertController::OnActorTabIndicatorStateChanged,
base::Unretained(this)));
}
#if BUILDFLAG(ENABLE_GLIC)
glic::GlicTabIndicatorHelper* const glic_tab_indicator_helper =
glic::GlicTabIndicatorHelper::From(&tab);
if (glic_tab_indicator_helper) {
callback_subscriptions_.emplace_back(
glic_tab_indicator_helper->RegisterGlicSharingStateChange(
base::BindRepeating(&TabAlertController::OnGlicSharingStateChange,
base::Unretained(this))));
callback_subscriptions_.emplace_back(
glic_tab_indicator_helper->RegisterGlicAccessingStateChange(
base::BindRepeating(&TabAlertController::OnGlicAccessingStateChange,
base::Unretained(this))));
}
#endif // BUILDFLAG(ENABLE_GLIC)
}
TabAlertController::~TabAlertController() = default;
// static:
const TabAlertController* TabAlertController::From(const TabInterface* tab) {
return Get(tab->GetUnownedUserDataHost());
}
// static:
TabAlertController* TabAlertController::From(TabInterface* tab) {
return Get(tab->GetUnownedUserDataHost());
}
// static:
std::u16string TabAlertController::GetTabAlertStateText(
const tabs::TabAlert alert_state) {
switch (alert_state) {
case tabs::TabAlert::kAudioPlaying:
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_AUDIO_PLAYING);
case tabs::TabAlert::kAudioMuting:
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_AUDIO_MUTING);
case tabs::TabAlert::kMediaRecording:
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_MEDIA_RECORDING);
case tabs::TabAlert::kAudioRecording:
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_AUDIO_RECORDING);
case tabs::TabAlert::kVideoRecording:
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_VIDEO_RECORDING);
case tabs::TabAlert::kTabCapturing:
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_TAB_CAPTURING);
case tabs::TabAlert::kBluetoothConnected:
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_BLUETOOTH_CONNECTED);
case tabs::TabAlert::kBluetoothScanActive:
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_BLUETOOTH_SCAN_ACTIVE);
case tabs::TabAlert::kUsbConnected:
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_USB_CONNECTED);
case tabs::TabAlert::kHidConnected:
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_HID_CONNECTED);
case tabs::TabAlert::kSerialConnected:
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_SERIAL_CONNECTED);
case tabs::TabAlert::kPipPlaying:
return l10n_util::GetStringUTF16(IDS_TOOLTIP_TAB_ALERT_STATE_PIP_PLAYING);
case tabs::TabAlert::kDesktopCapturing:
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_DESKTOP_CAPTURING);
case tabs::TabAlert::kVrPresentingInHeadset:
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_VR_PRESENTING);
// TODO(crbug.com/461457730) Create new resources for ACTOR_ACCESSING of
// relying on GLIC_ACCESSING resources below.
case tabs::TabAlert::kActorAccessing:
case tabs::TabAlert::kActorWaitingOnUser:
case tabs::TabAlert::kGlicAccessing:
#if BUILDFLAG(ENABLE_GLIC)
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_GLIC_ACCESSING);
#else
return u"";
#endif
case tabs::TabAlert::kGlicSharing:
#if BUILDFLAG(ENABLE_GLIC)
return l10n_util::GetStringUTF16(
IDS_TOOLTIP_TAB_ALERT_STATE_GLIC_SHARING);
#else
return u"";
#endif
}
NOTREACHED();
}
base::CallbackListSubscription
TabAlertController::AddAlertToShowChangedCallback(
AlertToShowChangedCallback callback) {
return alert_to_show_changed_callbacks_.Add(std::move(callback));
}
std::optional<TabAlert> TabAlertController::GetAlertToShow() const {
if (active_alerts_.empty()) {
return std::nullopt;
}
return *active_alerts_.begin();
}
std::vector<TabAlert> TabAlertController::GetAllActiveAlerts() const {
return base::ToVector(active_alerts_);
}
bool TabAlertController::IsAlertActive(TabAlert alert) const {
return active_alerts_.contains(alert);
}
void TabAlertController::OnDiscardContents(TabInterface* tab_interface,
content::WebContents* old_contents,
content::WebContents* new_contents) {
tabs::ContentsObservingTabFeature::OnDiscardContents(
tab_interface, old_contents, new_contents);
vr_tab_helper_observation_.Reset();
vr_tab_helper_observation_.Observe(
vr::VrTabHelper::FromWebContents(new_contents));
recently_audible_subscription_ =
RecentlyAudibleHelper::FromWebContents(new_contents)
->RegisterRecentlyAudibleChangedCallback(base::BindRepeating(
&TabAlertController::OnRecentlyAudibleStateChanged,
base::Unretained(this)));
}
void TabAlertController::OnCapabilityTypesChanged(
content::WebContentsCapabilityType capability_type,
bool used) {
static constexpr base::fixed_flat_map<content::WebContentsCapabilityType,
TabAlert, 5>
capability_type_to_alert =
base::MakeFixedFlatMap<content::WebContentsCapabilityType, TabAlert>(
{{content::WebContentsCapabilityType::kBluetoothConnected,
TabAlert::kBluetoothConnected},
{content::WebContentsCapabilityType::kBluetoothScanning,
TabAlert::kBluetoothScanActive},
{content::WebContentsCapabilityType::kUSB,
TabAlert::kUsbConnected},
{content::WebContentsCapabilityType::kHID,
TabAlert::kHidConnected},
{content::WebContentsCapabilityType::kSerial,
TabAlert::kSerialConnected}});
if (!capability_type_to_alert.contains(capability_type)) {
return;
}
const TabAlert alert = capability_type_to_alert.at(capability_type);
UpdateAlertState(alert, used);
}
void TabAlertController::MediaPictureInPictureChanged(
bool is_picture_in_picture) {
UpdateAlertState(TabAlert::kPipPlaying, is_picture_in_picture);
}
void TabAlertController::DidUpdateAudioMutingState(bool muted) {
// The muted alert should only show for tabs that were recently audible. It is
// possible for a tab to be muted but never play audio, in such cases, the
// muted alert should not show.
RecentlyAudibleHelper* const audible_helper =
RecentlyAudibleHelper::FromWebContents(tab().GetContents());
CHECK(audible_helper);
UpdateAlertState(TabAlert::kAudioMuting,
audible_helper->WasRecentlyAudible() && muted);
}
void TabAlertController::OnIsCapturingVideoChanged(
content::WebContents* contents,
bool is_capturing_video) {
if (contents == web_contents()) {
UpdateAlertState(TabAlert::kVideoRecording, is_capturing_video);
}
}
void TabAlertController::OnIsCapturingAudioChanged(
content::WebContents* contents,
bool is_capturing_audio) {
if (contents == web_contents()) {
UpdateAlertState(TabAlert::kAudioRecording, is_capturing_audio);
}
}
void TabAlertController::OnIsBeingMirroredChanged(
content::WebContents* contents,
bool is_being_mirrored) {
if (contents == web_contents()) {
UpdateAlertState(TabAlert::kTabCapturing, is_being_mirrored);
}
}
void TabAlertController::OnIsCapturingWindowChanged(
content::WebContents* contents,
bool is_capturing_window) {
if (contents == web_contents()) {
const bool is_desktop_capturing_active =
is_capturing_window || MediaCaptureDevicesDispatcher::GetInstance()
->GetMediaStreamCaptureIndicator()
->IsCapturingDisplay(contents);
UpdateAlertState(TabAlert::kDesktopCapturing, is_desktop_capturing_active);
}
}
void TabAlertController::OnIsCapturingDisplayChanged(
content::WebContents* contents,
bool is_capturing_display) {
if (contents == web_contents()) {
const bool is_desktop_capturing_active =
is_capturing_display || MediaCaptureDevicesDispatcher::GetInstance()
->GetMediaStreamCaptureIndicator()
->IsCapturingWindow(contents);
UpdateAlertState(TabAlert::kDesktopCapturing, is_desktop_capturing_active);
}
}
void TabAlertController::OnIsContentDisplayedInHeadsetChanged(bool state) {
UpdateAlertState(TabAlert::kVrPresentingInHeadset, state);
}
#if BUILDFLAG(ENABLE_GLIC)
void TabAlertController::OnGlicSharingStateChange(bool is_sharing) {
UpdateAlertState(TabAlert::kGlicSharing, is_sharing);
}
void TabAlertController::OnGlicAccessingStateChange(bool is_accessing) {
UpdateAlertState(TabAlert::kGlicAccessing, is_accessing);
}
#endif // BUILDFLAG(ENABLE_GLIC)
void TabAlertController::OnActorTabIndicatorStateChanged(
actor::ui::TabIndicatorStatus tab_indicator_status) {
switch (tab_indicator_status) {
case actor::ui::TabIndicatorStatus::kNone:
UpdateAlertState(TabAlert::kActorWaitingOnUser, false);
UpdateAlertState(TabAlert::kActorAccessing, false);
break;
case actor::ui::TabIndicatorStatus::kDynamic:
UpdateAlertState(TabAlert::kActorWaitingOnUser, false);
UpdateAlertState(TabAlert::kActorAccessing, true);
break;
case actor::ui::TabIndicatorStatus::kStatic:
UpdateAlertState(TabAlert::kActorWaitingOnUser, true);
UpdateAlertState(TabAlert::kActorAccessing, false);
break;
}
}
void TabAlertController::OnRecentlyAudibleStateChanged(bool was_audible) {
// Muted alert state also needs to update when audible state changes to ensure
// that the muted alert becomes active if the tab is already muted but is
// recently audible or inactive after the tab is no longer audible.
DidUpdateAudioMutingState(tab().GetContents()->IsAudioMuted());
UpdateAlertState(TabAlert::kAudioPlaying, was_audible);
}
void TabAlertController::UpdateAlertState(TabAlert alert, bool is_active) {
std::optional<TabAlert> previous_alert = GetAlertToShow();
if (alert == TabAlert::kAudioRecording ||
alert == TabAlert::kVideoRecording) {
UpdateMediaAlert();
} else {
if (is_active) {
active_alerts_.insert(alert);
} else {
active_alerts_.erase(alert);
}
}
std::optional<TabAlert> updated_alert = GetAlertToShow();
if (previous_alert != updated_alert) {
alert_to_show_changed_callbacks_.Notify(updated_alert);
}
}
void TabAlertController::UpdateMediaAlert() {
MediaStreamCaptureIndicator* const media_stream_capture_indicator =
MediaCaptureDevicesDispatcher::GetInstance()
->GetMediaStreamCaptureIndicator()
.get();
content::WebContents* const web_contents = tab().GetContents();
const bool is_capturing_audio =
media_stream_capture_indicator->IsCapturingAudio(web_contents);
const bool is_capturing_video =
media_stream_capture_indicator->IsCapturingVideo(web_contents);
active_alerts_.erase(TabAlert::kMediaRecording);
active_alerts_.erase(TabAlert::kVideoRecording);
active_alerts_.erase(TabAlert::kAudioRecording);
if (is_capturing_video && is_capturing_audio) {
active_alerts_.insert(TabAlert::kMediaRecording);
} else if (is_capturing_video) {
active_alerts_.insert(TabAlert::kVideoRecording);
} else if (is_capturing_audio) {
active_alerts_.insert(TabAlert::kAudioRecording);
}
}
} // namespace tabs