#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
#include "base/auto_reset.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/notimplemented.h"
#include "base/observer_list.h"
#include "base/task/single_thread_task_runner.h"
#include "build/build_config.h"
#include "chrome/browser/app_mode/app_mode_utils.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_context.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_tab_params.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_within_tab_helper.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/browser/history_types.h"
#include "content/public/browser/fullscreen_types.h"
#include "content/public/browser/navigation_details.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/render_frame_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/types/display_constants.h"
#if !BUILDFLAG(IS_MAC)
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#endif
#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/blocked_content/popunder_preventer.h"
#endif
#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "components/safe_browsing/content/browser/safe_browsing_service_interface.h"
#endif
using content::WebContents;
namespace {
bool IsAnotherScreen(const WebContents& web_contents,
const int64_t display_id) {
if (display_id == display::kInvalidDisplayId) {
return false;
}
return display_id != FullscreenController::GetDisplayId(web_contents);
}
}
FullscreenController::FullscreenController(ExclusiveAccessManager* manager)
: ExclusiveAccessControllerBase(manager) {}
FullscreenController::~FullscreenController() = default;
void FullscreenController::AddObserver(FullscreenObserver* observer) {
observer_list_.AddObserver(observer);
}
void FullscreenController::RemoveObserver(FullscreenObserver* observer) {
observer_list_.RemoveObserver(observer);
}
int64_t FullscreenController::GetDisplayId(const WebContents& web_contents) {
if (auto* screen = display::Screen::Get()) {
auto display = screen->GetDisplayNearestView(
const_cast<WebContents&>(web_contents).GetNativeView());
return display.id();
}
return display::kInvalidDisplayId;
}
bool FullscreenController::IsFullscreenForBrowser() const {
return exclusive_access_manager()->context()->IsFullscreen() &&
!IsFullscreenCausedByTab();
}
void FullscreenController::ToggleBrowserFullscreenMode(bool user_initiated) {
extension_url_.reset();
ToggleFullscreenModeInternal(FullscreenInternalOption::kBrowser, nullptr,
display::kInvalidDisplayId, user_initiated);
}
void FullscreenController::ToggleBrowserFullscreenModeWithExtension(
const GURL& extension_url) {
extension_url_ = extension_url;
ToggleFullscreenModeInternal(FullscreenInternalOption::kBrowser, nullptr,
display::kInvalidDisplayId,
false);
}
bool FullscreenController::IsWindowFullscreenForTabOrPending() const {
return exclusive_access_tab() || is_tab_fullscreen_for_testing_;
}
bool FullscreenController::IsExtensionFullscreenOrPending() const {
return extension_url_.has_value();
}
bool FullscreenController::IsControllerInitiatedFullscreen() const {
return toggled_into_fullscreen_;
}
bool FullscreenController::IsTabFullscreen() const {
return tab_fullscreen_ || is_tab_fullscreen_for_testing_;
}
content::FullscreenState FullscreenController::GetFullscreenState(
const content::WebContents* web_contents) const {
content::FullscreenState state;
CHECK(web_contents) << "Null web_contents passed to GetFullscreenState";
if (IsFullscreenWithinTab(web_contents)) {
state.target_mode = content::FullscreenMode::kPseudoContent;
return state;
}
if (!tab_fullscreen_ || web_contents != exclusive_access_tab()) {
state.target_mode = content::FullscreenMode::kWindowed;
return state;
}
state.target_mode = content::FullscreenMode::kContent;
state.target_display_id =
(tab_fullscreen_target_display_id_ != display::kInvalidDisplayId)
? tab_fullscreen_target_display_id_
: GetDisplayId(*web_contents);
return state;
}
bool FullscreenController::IsFullscreenCausedByTab() const {
return state_prior_to_tab_fullscreen_ == STATE_NORMAL;
}
bool FullscreenController::CanEnterFullscreenModeForTab(
content::RenderFrameHost* requesting_frame) {
DCHECK(requesting_frame);
auto* web_contents = WebContents::FromRenderFrameHost(requesting_frame);
DCHECK(web_contents);
if (web_contents != exclusive_access_manager()
->context()
->GetWebContentsForExclusiveAccess()) {
return false;
}
return true;
}
void FullscreenController::EnterFullscreenModeForTab(
content::RenderFrameHost* requesting_frame,
FullscreenTabParams fullscreen_tab_params) {
DCHECK(requesting_frame);
DCHECK(CanEnterFullscreenModeForTab(requesting_frame));
auto* web_contents = WebContents::FromRenderFrameHost(requesting_frame);
DCHECK(web_contents);
if (MaybeToggleFullscreenWithinTab(web_contents, true)) {
return;
}
#if !BUILDFLAG(IS_ANDROID)
if (!popunder_preventer_) {
popunder_preventer_ = std::make_unique<PopunderPreventer>(web_contents);
} else {
popunder_preventer_->WillActivateWebContents(web_contents);
}
#endif
const bool requesting_another_screen =
IsAnotherScreen(*web_contents, fullscreen_tab_params.display_id);
const bool was_window_fullscreen_for_tab_or_pending =
!requesting_another_screen && IsWindowFullscreenForTabOrPending();
if (exclusive_access_tab() && exclusive_access_tab() != web_contents) {
NOTIMPLEMENTED() << "Conflicting exclusive access tab assignment detected";
SetTabWithExclusiveAccess(nullptr);
}
SetTabWithExclusiveAccess(web_contents);
requesting_origin_ = requesting_frame->GetLastCommittedOrigin();
if (was_window_fullscreen_for_tab_or_pending) {
DCHECK(tab_fullscreen_);
#if BUILDFLAG(IS_ANDROID)
DCHECK(fullscreen_parameters_.has_value());
if (fullscreen_parameters_.has_value() &&
fullscreen_tab_params != fullscreen_parameters_) {
EnterFullscreenModeInternal(FullscreenInternalOption::kTab,
requesting_frame, fullscreen_tab_params);
}
#endif
} else {
ExclusiveAccessContext* exclusive_access_context =
exclusive_access_manager()->context();
exclusive_access_context->UpdateUIForTabFullscreen();
state_prior_to_tab_fullscreen_ =
IsFullscreenForBrowser() ? STATE_BROWSER_FULLSCREEN : STATE_NORMAL;
if (!exclusive_access_context->IsFullscreen() ||
requesting_another_screen) {
EnterFullscreenModeInternal(FullscreenInternalOption::kTab,
requesting_frame, fullscreen_tab_params);
return;
}
tab_fullscreen_ = true;
exclusive_access_manager()->UpdateBubble(base::NullCallback());
}
PostFullscreenChangeNotification();
}
void FullscreenController::ExitFullscreenModeForTab(WebContents* web_contents) {
if (MaybeToggleFullscreenWithinTab(web_contents, false)) {
return;
}
if (!IsWindowFullscreenForTabOrPending() ||
web_contents != exclusive_access_tab()) {
return;
}
ExclusiveAccessContext* exclusive_access_context =
exclusive_access_manager()->context();
if (!exclusive_access_context->IsFullscreen()) {
return;
}
if (IsFullscreenCausedByTab()) {
ExitFullscreenModeInternal();
return;
}
const bool was_browser_fullscreen =
state_prior_to_tab_fullscreen_ == STATE_BROWSER_FULLSCREEN;
NotifyTabExclusiveAccessLost();
if (was_browser_fullscreen) {
exclusive_access_context->UpdateUIForTabFullscreen();
}
if (was_browser_fullscreen && web_contents &&
display_id_prior_to_tab_fullscreen_ != display::kInvalidDisplayId &&
display_id_prior_to_tab_fullscreen_ != GetDisplayId(*web_contents)) {
EnterFullscreenModeInternal(
FullscreenInternalOption::kBrowser, nullptr,
FullscreenTabParams{display_id_prior_to_tab_fullscreen_});
return;
}
PostFullscreenChangeNotification();
}
#if !BUILDFLAG(IS_ANDROID)
void FullscreenController::FullscreenTabOpeningPopup(
content::WebContents* opener,
content::WebContents* popup) {
if (!popunder_preventer_) {
return;
}
DCHECK_EQ(exclusive_access_tab(), opener);
popunder_preventer_->AddPotentialPopunder(popup);
}
#endif
void FullscreenController::OnTabDeactivated(
content::WebContents* web_contents) {
base::AutoReset<raw_ptr<content::WebContents>> auto_resetter(
&deactivated_contents_, web_contents);
ExclusiveAccessControllerBase::OnTabDeactivated(web_contents);
}
void FullscreenController::OnTabDetachedFromView(WebContents* old_contents) {
if (!IsFullscreenWithinTab(old_contents)) {
return;
}
if (!old_contents->GetDelegate()) {
return;
}
if (!old_contents->IsBeingCaptured() ||
old_contents->GetPreferredSize().IsEmpty()) {
return;
}
old_contents->Resize(gfx::Rect(old_contents->GetPreferredSize()));
}
void FullscreenController::OnTabClosing(WebContents* web_contents) {
if (IsFullscreenWithinTab(web_contents)) {
web_contents->ExitFullscreen(
IsFullscreenCausedByTab());
} else {
ExclusiveAccessControllerBase::OnTabClosing(web_contents);
}
}
void FullscreenController::WindowFullscreenStateChanged() {
ExclusiveAccessContext* const exclusive_access_context =
exclusive_access_manager()->context();
bool exiting_fullscreen = !exclusive_access_context->IsFullscreen();
PostFullscreenChangeNotification();
if (exiting_fullscreen) {
toggled_into_fullscreen_ = false;
extension_url_.reset();
NotifyTabExclusiveAccessLost();
} else {
toggled_into_fullscreen_ = true;
if (!IsRunningInAppMode()) {
exclusive_access_manager()->UpdateBubble(base::NullCallback(),
true);
}
if (!fullscreen_start_time_) {
fullscreen_start_time_ = base::TimeTicks::Now();
}
}
}
void FullscreenController::FullscreenTransitionCompleted() {
if (fullscreen_transition_complete_callback_) {
std::move(fullscreen_transition_complete_callback_).Run();
}
#if DCHECK_IS_ON()
if (started_fullscreen_transition_ && IsTabFullscreen()) {
DCHECK(exclusive_access_tab());
DCHECK_EQ(tab_fullscreen_target_display_id_,
GetDisplayId(*exclusive_access_tab()));
}
#endif
tab_fullscreen_target_display_id_ = display::kInvalidDisplayId;
started_fullscreen_transition_ = false;
#if !BUILDFLAG(IS_ANDROID)
if (!IsTabFullscreen()) {
popunder_preventer_.reset();
}
#endif
}
void FullscreenController::RunOrDeferUntilTransitionIsComplete(
base::OnceClosure callback) {
if (started_fullscreen_transition_) {
fullscreen_transition_complete_callback_ = std::move(callback);
} else {
std::move(callback).Run();
}
}
bool FullscreenController::HandleUserPressedEscape() {
WebContents* const active_web_contents =
exclusive_access_manager()->context()->GetWebContentsForExclusiveAccess();
if (IsFullscreenWithinTab(active_web_contents)) {
active_web_contents->ExitFullscreen(
IsFullscreenCausedByTab());
return true;
}
if (!IsWindowFullscreenForTabOrPending()) {
return false;
}
ExitExclusiveAccessIfNecessary();
base::RecordAction(base::UserMetricsAction("ExitFullscreen_Esc"));
return true;
}
void FullscreenController::HandleUserHeldEscape() {
CHECK(exclusive_access_manager());
CHECK(exclusive_access_manager()->context());
ExclusiveAccessContext* const exclusive_access_context =
exclusive_access_manager()->context();
if (RequiresPressAndHoldEscToExit() &&
exclusive_access_context->CanUserExitFullscreen()) {
ExitFullscreenModeInternal();
base::RecordAction(
base::UserMetricsAction("ExitFullscreen_PressAndHoldEsc"));
}
}
void FullscreenController::HandleUserReleasedEscapeEarly() {}
bool FullscreenController::RequiresPressAndHoldEscToExit() const {
return IsFullscreenForBrowser();
}
void FullscreenController::ExitExclusiveAccessToPreviousState() {
if (IsWindowFullscreenForTabOrPending()) {
ExitFullscreenModeForTab(exclusive_access_tab());
} else if (IsFullscreenForBrowser()) {
ExitFullscreenModeInternal();
}
}
url::Origin FullscreenController::GetOriginForExclusiveAccessBubble() const {
if (exclusive_access_tab()) {
return GetRequestingOrigin();
}
if (extension_url_.has_value()) {
return url::Origin::Create(extension_url_.value());
}
return url::Origin();
}
void FullscreenController::ExitExclusiveAccessIfNecessary() {
if (IsWindowFullscreenForTabOrPending()) {
ExitFullscreenModeForTab(exclusive_access_tab());
} else {
NotifyTabExclusiveAccessLost();
}
}
void FullscreenController::PostFullscreenChangeNotification() {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&FullscreenController::NotifyFullscreenChange,
ptr_factory_.GetWeakPtr()));
}
void FullscreenController::NotifyFullscreenChange() {
for (auto& observer : observer_list_) {
observer.OnFullscreenStateChanged();
}
}
void FullscreenController::NotifyTabExclusiveAccessLost() {
if (exclusive_access_tab()) {
WebContents* web_contents = exclusive_access_tab();
SetTabWithExclusiveAccess(nullptr);
requesting_origin_ = url::Origin();
bool will_cause_resize = IsFullscreenCausedByTab();
state_prior_to_tab_fullscreen_ = STATE_INVALID;
tab_fullscreen_ = false;
web_contents->ExitFullscreen(will_cause_resize);
exclusive_access_manager()->UpdateBubble(base::NullCallback());
}
}
void FullscreenController::ToggleFullscreenModeInternal(
FullscreenInternalOption option,
content::RenderFrameHost* requesting_frame,
const int64_t display_id,
bool user_initiated) {
ExclusiveAccessContext* const exclusive_access_context =
exclusive_access_manager()->context();
bool enter_fullscreen = !exclusive_access_context->IsFullscreen();
if (enter_fullscreen &&
(exclusive_access_context->CanUserEnterFullscreen() || !user_initiated)) {
EnterFullscreenModeInternal(option, requesting_frame,
FullscreenTabParams{display_id});
}
if (!enter_fullscreen &&
(exclusive_access_context->CanUserExitFullscreen() || !user_initiated)) {
ExitFullscreenModeInternal();
}
}
void FullscreenController::EnterFullscreenModeInternal(
FullscreenInternalOption option,
content::RenderFrameHost* requesting_frame,
FullscreenTabParams fullscreen_tab_params) {
#if !BUILDFLAG(IS_MAC)
Profile* profile = exclusive_access_manager()->context()->GetProfile();
if (!profile || !profile->GetPrefs()->GetBoolean(prefs::kFullscreenAllowed)) {
return;
}
#endif
fullscreen_parameters_ = fullscreen_tab_params;
started_fullscreen_transition_ = true;
toggled_into_fullscreen_ = true;
bool entering_tab_fullscreen =
option == FullscreenInternalOption::kTab && !tab_fullscreen_;
url::Origin origin;
if (option == FullscreenInternalOption::kTab) {
origin = GetRequestingOrigin();
tab_fullscreen_ = true;
WebContents* web_contents =
WebContents::FromRenderFrameHost(requesting_frame);
DCHECK(web_contents);
if (!web_contents) {
return;
}
int64_t display_id = fullscreen_tab_params.display_id;
int64_t current_display = GetDisplayId(*web_contents);
if (display_id != display::kInvalidDisplayId) {
if (!requesting_frame ||
requesting_frame->GetBrowserContext()
->GetPermissionController()
->GetPermissionStatusForCurrentDocument(
content::PermissionDescriptorUtil::
CreatePermissionDescriptorForPermissionType(
blink::PermissionType::WINDOW_MANAGEMENT),
requesting_frame) !=
blink::mojom::PermissionStatus::GRANTED) {
display_id = display::kInvalidDisplayId;
} else if (entering_tab_fullscreen) {
display_id_prior_to_tab_fullscreen_ = current_display;
}
}
tab_fullscreen_target_display_id_ =
display_id == display::kInvalidDisplayId ? current_display : display_id;
} else {
if (extension_url_) {
origin = url::Origin::Create(extension_url_.value());
}
}
fullscreen_start_time_ = base::TimeTicks::Now();
if (option == FullscreenInternalOption::kBrowser) {
base::RecordAction(base::UserMetricsAction("ToggleFullscreen"));
}
exclusive_access_manager()->context()->EnterFullscreen(
origin, exclusive_access_manager()->GetExclusiveAccessExitBubbleType(),
fullscreen_tab_params);
}
void FullscreenController::ExitFullscreenModeInternal() {
if (IsRunningInAppMode()) {
return;
}
if (fullscreen_start_time_ && exclusive_access_tab()) {
ukm::SourceId source_id =
exclusive_access_tab()->GetPrimaryMainFrame()->GetPageUkmSourceId();
ukm::builders::Fullscreen_Exit(source_id)
.SetSessionDuration(ukm::GetSemanticBucketMinForDurationTiming(
(base::TimeTicks::Now() - fullscreen_start_time_.value())
.InMilliseconds()))
.Record(ukm::UkmRecorder::Get());
fullscreen_start_time_.reset();
}
fullscreen_parameters_.reset();
toggled_into_fullscreen_ = false;
started_fullscreen_transition_ = true;
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_ANDROID)
NotifyTabExclusiveAccessLost();
#endif
exclusive_access_manager()->context()->ExitFullscreen();
extension_url_.reset();
exclusive_access_manager()->UpdateBubble(base::NullCallback());
}
bool FullscreenController::MaybeToggleFullscreenWithinTab(
WebContents* web_contents,
bool enter_fullscreen) {
if (enter_fullscreen) {
if (web_contents->IsBeingVisiblyCaptured()) {
FullscreenWithinTabHelper::CreateForWebContents(web_contents);
FullscreenWithinTabHelper::FromWebContents(web_contents)
->SetIsFullscreenWithinTab(true);
return true;
}
} else {
if (IsFullscreenWithinTab(web_contents)) {
FullscreenWithinTabHelper::RemoveForWebContents(web_contents);
return true;
}
}
return false;
}
bool FullscreenController::IsFullscreenWithinTab(
const WebContents* web_contents) const {
if (is_tab_fullscreen_for_testing_) {
return true;
}
const FullscreenWithinTabHelper* const helper =
web_contents ? FullscreenWithinTabHelper::FromWebContents(web_contents)
: nullptr;
if (helper && helper->is_fullscreen_within_tab()) {
DCHECK_NE(exclusive_access_tab(), web_contents);
return true;
}
return false;
}
url::Origin FullscreenController::GetRequestingOrigin() const {
DCHECK(exclusive_access_tab());
if (!requesting_origin_.opaque()) {
return requesting_origin_;
}
return url::Origin::Create(exclusive_access_tab()->GetLastCommittedURL());
}
url::Origin FullscreenController::GetEmbeddingOrigin() const {
DCHECK(exclusive_access_tab());
return url::Origin::Create(exclusive_access_tab()->GetLastCommittedURL());
}