blob: 8f0f7fc7c516fc87e68f13b2a119d8f80e7d7e80 [file] [log] [blame]
// Copyright (c) 2012 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/ui/exclusive_access/fullscreen_controller.h"
#include "base/bind.h"
#include "base/command_line.h"
#include "base/message_loop/message_loop.h"
#include "chrome/browser/app_mode/app_mode_utils.h"
#include "chrome/browser/chrome_notification_types.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_within_tab_helper.h"
#include "chrome/browser/ui/status_bubble.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/web_contents_sizer.h"
#include "chrome/common/chrome_switches.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/user_metrics.h"
#include "content/public/browser/web_contents.h"
#include "extensions/common/extension.h"
#if !defined(OS_MACOSX)
#include "base/prefs/pref_service.h"
#include "chrome/common/pref_names.h"
#endif
using base::UserMetricsAction;
using content::RenderViewHost;
using content::WebContents;
FullscreenController::FullscreenController(ExclusiveAccessManager* manager)
: ExclusiveAccessControllerBase(manager),
state_prior_to_tab_fullscreen_(STATE_INVALID),
tab_fullscreen_accepted_(false),
toggled_into_fullscreen_(false),
reentrant_window_state_change_call_check_(false),
is_privileged_fullscreen_for_testing_(false),
ptr_factory_(this) {
}
FullscreenController::~FullscreenController() {
}
bool FullscreenController::IsFullscreenForBrowser() const {
return exclusive_access_manager()->context()->IsFullscreen() &&
!IsFullscreenCausedByTab();
}
void FullscreenController::ToggleBrowserFullscreenMode() {
extension_caused_fullscreen_ = GURL();
ToggleFullscreenModeInternal(BROWSER);
}
void FullscreenController::ToggleBrowserFullscreenWithToolbar() {
ToggleFullscreenModeInternal(BROWSER_WITH_TOOLBAR);
}
void FullscreenController::ToggleBrowserFullscreenModeWithExtension(
const GURL& extension_url) {
// |extension_caused_fullscreen_| will be reset if this causes fullscreen to
// exit.
extension_caused_fullscreen_ = extension_url;
ToggleFullscreenModeInternal(BROWSER);
}
bool FullscreenController::IsWindowFullscreenForTabOrPending() const {
return exclusive_access_tab() != nullptr;
}
bool FullscreenController::IsExtensionFullscreenOrPending() const {
return !extension_caused_fullscreen_.is_empty();
}
bool FullscreenController::IsControllerInitiatedFullscreen() const {
return toggled_into_fullscreen_;
}
bool FullscreenController::IsUserAcceptedFullscreen() const {
return tab_fullscreen_accepted_;
}
bool FullscreenController::IsFullscreenForTabOrPending(
const WebContents* web_contents) const {
if (web_contents == exclusive_access_tab()) {
DCHECK(web_contents ==
exclusive_access_manager()->context()->GetActiveWebContents());
DCHECK(web_contents->GetCapturerCount() == 0);
return true;
}
return IsFullscreenForCapturedTab(web_contents);
}
bool FullscreenController::IsFullscreenCausedByTab() const {
return state_prior_to_tab_fullscreen_ == STATE_NORMAL;
}
void FullscreenController::EnterFullscreenModeForTab(WebContents* web_contents,
const GURL& origin) {
DCHECK(web_contents);
if (MaybeToggleFullscreenForCapturedTab(web_contents, true)) {
// During tab capture of fullscreen-within-tab views, the browser window
// fullscreen state is unchanged, so return now.
return;
}
if (web_contents !=
exclusive_access_manager()->context()->GetActiveWebContents() ||
IsWindowFullscreenForTabOrPending()) {
return;
}
#if defined(OS_WIN)
// For now, avoid breaking when initiating full screen tab mode while in
// a metro snap.
// TODO(robertshield): Find a way to reconcile tab-initiated fullscreen
// modes with metro snap.
if (IsInMetroSnapMode())
return;
#endif
SetTabWithExclusiveAccess(web_contents);
fullscreened_origin_ = origin;
ExclusiveAccessContext* exclusive_access_context =
exclusive_access_manager()->context();
if (!exclusive_access_context->IsFullscreen()) {
// Normal -> Tab Fullscreen.
state_prior_to_tab_fullscreen_ = STATE_NORMAL;
ToggleFullscreenModeInternal(TAB);
return;
}
if (exclusive_access_context->IsFullscreenWithToolbar()) {
// Browser Fullscreen with Toolbar -> Tab Fullscreen (no toolbar).
exclusive_access_context->UpdateFullscreenWithToolbar(false);
state_prior_to_tab_fullscreen_ = STATE_BROWSER_FULLSCREEN_WITH_TOOLBAR;
} else {
// Browser Fullscreen without Toolbar -> Tab Fullscreen.
state_prior_to_tab_fullscreen_ = STATE_BROWSER_FULLSCREEN_NO_TOOLBAR;
}
// We need to update the fullscreen exit bubble, e.g., going from browser
// fullscreen to tab fullscreen will need to show different content.
if (!tab_fullscreen_accepted_) {
tab_fullscreen_accepted_ = GetFullscreenSetting() == CONTENT_SETTING_ALLOW;
}
exclusive_access_manager()->UpdateExclusiveAccessExitBubbleContent();
// This is only a change between Browser and Tab fullscreen. We generate
// a fullscreen notification now because there is no window change.
PostFullscreenChangeNotification(true);
}
void FullscreenController::ExitFullscreenModeForTab(WebContents* web_contents) {
if (MaybeToggleFullscreenForCapturedTab(web_contents, false)) {
// During tab capture of fullscreen-within-tab views, the browser window
// fullscreen state is unchanged, so return now.
return;
}
if (!IsWindowFullscreenForTabOrPending() ||
web_contents != exclusive_access_tab()) {
return;
}
#if defined(OS_WIN)
// For now, avoid breaking when initiating full screen tab mode while in
// a metro snap.
// TODO(robertshield): Find a way to reconcile tab-initiated fullscreen
// modes with metro snap.
if (IsInMetroSnapMode())
return;
#endif
ExclusiveAccessContext* exclusive_access_context =
exclusive_access_manager()->context();
if (!exclusive_access_context->IsFullscreen())
return;
if (IsFullscreenCausedByTab()) {
// Tab Fullscreen -> Normal.
ToggleFullscreenModeInternal(TAB);
return;
}
// Tab Fullscreen -> Browser Fullscreen (with or without toolbar).
if (state_prior_to_tab_fullscreen_ == STATE_BROWSER_FULLSCREEN_WITH_TOOLBAR) {
// Tab Fullscreen (no toolbar) -> Browser Fullscreen with Toolbar.
exclusive_access_context->UpdateFullscreenWithToolbar(true);
}
#if defined(OS_MACOSX)
// Clear the bubble URL, which forces the Mac UI to redraw.
exclusive_access_manager()->UpdateExclusiveAccessExitBubbleContent();
#endif // defined(OS_MACOSX)
// If currently there is a tab in "tab fullscreen" mode and fullscreen
// was not caused by it (i.e., previously it was in "browser fullscreen"
// mode), we need to switch back to "browser fullscreen" mode. In this
// case, all we have to do is notifying the tab that it has exited "tab
// fullscreen" mode.
NotifyTabExclusiveAccessLost();
// This is only a change between Browser and Tab fullscreen. We generate
// a fullscreen notification now because there is no window change.
PostFullscreenChangeNotification(true);
}
#if defined(OS_WIN)
bool FullscreenController::IsInMetroSnapMode() {
return exclusive_access_manager()->context()->IsInMetroSnapMode();
}
void FullscreenController::SetMetroSnapMode(bool enable) {
reentrant_window_state_change_call_check_ = false;
toggled_into_fullscreen_ = false;
exclusive_access_manager()->context()->SetMetroSnapMode(enable);
// FullscreenController unit tests for metro snap assume that on Windows calls
// to WindowFullscreenStateChanged are reentrant. If that assumption is
// invalidated, the tests must be updated to maintain coverage.
CHECK(reentrant_window_state_change_call_check_);
}
#endif // defined(OS_WIN)
void FullscreenController::OnTabDetachedFromView(WebContents* old_contents) {
if (!IsFullscreenForCapturedTab(old_contents))
return;
// A fullscreen-within-tab view undergoing screen capture has been detached
// and is no longer visible to the user. Set it to exactly the WebContents'
// preferred size. See 'FullscreenWithinTab Note'.
//
// When the user later selects the tab to show |old_contents| again, UI code
// elsewhere (e.g., views::WebView) will resize the view to fit within the
// browser window once again.
// If the view has been detached from the browser window (e.g., to drag a tab
// off into a new browser window), return immediately to avoid an unnecessary
// resize.
if (!old_contents->GetDelegate())
return;
// Do nothing if tab capture ended after toggling fullscreen, or a preferred
// size was never specified by the capturer.
if (old_contents->GetCapturerCount() == 0 ||
old_contents->GetPreferredSize().IsEmpty()) {
return;
}
content::RenderWidgetHostView* const current_fs_view =
old_contents->GetFullscreenRenderWidgetHostView();
if (current_fs_view)
current_fs_view->SetSize(old_contents->GetPreferredSize());
ResizeWebContents(old_contents, old_contents->GetPreferredSize());
}
void FullscreenController::OnTabClosing(WebContents* web_contents) {
if (IsFullscreenForCapturedTab(web_contents)) {
web_contents->ExitFullscreen();
} else {
ExclusiveAccessControllerBase::OnTabClosing(web_contents);
}
}
void FullscreenController::WindowFullscreenStateChanged() {
reentrant_window_state_change_call_check_ = true;
ExclusiveAccessContext* const exclusive_access_context =
exclusive_access_manager()->context();
bool exiting_fullscreen = !exclusive_access_context->IsFullscreen();
PostFullscreenChangeNotification(!exiting_fullscreen);
if (exiting_fullscreen) {
toggled_into_fullscreen_ = false;
extension_caused_fullscreen_ = GURL();
NotifyTabExclusiveAccessLost();
exclusive_access_context->UnhideDownloadShelf();
} else {
exclusive_access_context->HideDownloadShelf();
}
}
bool FullscreenController::HandleUserPressedEscape() {
WebContents* const active_web_contents =
exclusive_access_manager()->context()->GetActiveWebContents();
if (IsFullscreenForCapturedTab(active_web_contents)) {
active_web_contents->ExitFullscreen();
return true;
} else if (IsWindowFullscreenForTabOrPending()) {
ExitExclusiveAccessIfNecessary();
return true;
}
return false;
}
void FullscreenController::ExitExclusiveAccessToPreviousState() {
if (IsWindowFullscreenForTabOrPending())
ExitFullscreenModeForTab(exclusive_access_tab());
else if (IsFullscreenForBrowser())
ExitFullscreenModeInternal();
}
bool FullscreenController::OnAcceptExclusiveAccessPermission() {
ExclusiveAccessBubbleType bubble_type =
exclusive_access_manager()->GetExclusiveAccessExitBubbleType();
bool fullscreen = false;
exclusive_access_bubble::PermissionRequestedByType(bubble_type, &fullscreen,
nullptr);
DCHECK(!(fullscreen && tab_fullscreen_accepted_));
if (fullscreen && !tab_fullscreen_accepted_) {
DCHECK(exclusive_access_tab());
// Origins can enter fullscreen even when embedded in other origins.
// Permission is tracked based on the combinations of requester and
// embedder. Thus, even if a requesting origin has been previously approved
// for embedder A, it will not be approved when embedded in a different
// origin B.
//
// However, an exception is made when a requester and an embedder are the
// same origin. In other words, if the requester is the top-level frame. If
// that combination is ALLOWED, then future requests from that origin will
// succeed no matter what the embedder is. For example, if youtube.com
// is visited and user selects ALLOW. Later user visits example.com which
// embeds youtube.com in an iframe, which is then ALLOWED to go fullscreen.
GURL requester = GetRequestingOrigin();
GURL embedder = GetEmbeddingOrigin();
ContentSettingsPattern primary_pattern =
ContentSettingsPattern::FromURLNoWildcard(requester);
ContentSettingsPattern secondary_pattern =
ContentSettingsPattern::FromURLNoWildcard(embedder);
// ContentSettings requires valid patterns and the patterns might be invalid
// in some edge cases like if the current frame is about:blank.
//
// Do not store preference on file:// URLs, they don't have a clean
// origin policy.
// TODO(estark): Revisit this when crbug.com/455882 is fixed.
if (!requester.SchemeIsFile() && !embedder.SchemeIsFile() &&
primary_pattern.IsValid() && secondary_pattern.IsValid()) {
HostContentSettingsMap* settings_map = exclusive_access_manager()
->context()
->GetProfile()
->GetHostContentSettingsMap();
settings_map->SetContentSetting(
primary_pattern, secondary_pattern, CONTENT_SETTINGS_TYPE_FULLSCREEN,
std::string(), CONTENT_SETTING_ALLOW);
}
tab_fullscreen_accepted_ = true;
return true;
}
return false;
}
bool FullscreenController::OnDenyExclusiveAccessPermission() {
if (IsWindowFullscreenForTabOrPending()) {
ExitExclusiveAccessIfNecessary();
return true;
}
return false;
}
GURL FullscreenController::GetURLForExclusiveAccessBubble() const {
if (exclusive_access_tab())
return GetRequestingOrigin();
return extension_caused_fullscreen_;
}
void FullscreenController::ExitExclusiveAccessIfNecessary() {
if (IsWindowFullscreenForTabOrPending())
ExitFullscreenModeForTab(exclusive_access_tab());
else
NotifyTabExclusiveAccessLost();
}
void FullscreenController::PostFullscreenChangeNotification(
bool is_fullscreen) {
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(&FullscreenController::NotifyFullscreenChange,
ptr_factory_.GetWeakPtr(),
is_fullscreen));
}
void FullscreenController::NotifyFullscreenChange(bool is_fullscreen) {
content::NotificationService::current()->Notify(
chrome::NOTIFICATION_FULLSCREEN_CHANGED,
content::Source<FullscreenController>(this),
content::Details<bool>(&is_fullscreen));
}
void FullscreenController::NotifyTabExclusiveAccessLost() {
if (exclusive_access_tab()) {
WebContents* web_contents = exclusive_access_tab();
SetTabWithExclusiveAccess(nullptr);
fullscreened_origin_ = GURL();
state_prior_to_tab_fullscreen_ = STATE_INVALID;
tab_fullscreen_accepted_ = false;
web_contents->ExitFullscreen();
exclusive_access_manager()->UpdateExclusiveAccessExitBubbleContent();
}
}
void FullscreenController::ToggleFullscreenModeInternal(
FullscreenInternalOption option) {
#if defined(OS_WIN)
// When in Metro snap mode, toggling in and out of fullscreen is prevented.
if (IsInMetroSnapMode())
return;
#endif
ExclusiveAccessContext* const exclusive_access_context =
exclusive_access_manager()->context();
bool enter_fullscreen = !exclusive_access_context->IsFullscreen();
// When a Mac user requests a toggle they may be toggling between
// FullscreenWithoutChrome and FullscreenWithToolbar.
if (exclusive_access_context->IsFullscreen() &&
!IsWindowFullscreenForTabOrPending() &&
exclusive_access_context->SupportsFullscreenWithToolbar()) {
if (option == BROWSER_WITH_TOOLBAR) {
enter_fullscreen = enter_fullscreen ||
!exclusive_access_context->IsFullscreenWithToolbar();
} else {
enter_fullscreen = enter_fullscreen ||
exclusive_access_context->IsFullscreenWithToolbar();
}
}
// In kiosk mode, we always want to be fullscreen. When the browser first
// starts we're not yet fullscreen, so let the initial toggle go through.
if (chrome::IsRunningInAppMode() && exclusive_access_context->IsFullscreen())
return;
#if !defined(OS_MACOSX)
// Do not enter fullscreen mode if disallowed by pref. This prevents the user
// from manually entering fullscreen mode and also disables kiosk mode on
// desktop platforms.
if (enter_fullscreen &&
!exclusive_access_context->GetProfile()->GetPrefs()->GetBoolean(
prefs::kFullscreenAllowed)) {
return;
}
#endif
if (enter_fullscreen)
EnterFullscreenModeInternal(option);
else
ExitFullscreenModeInternal();
}
void FullscreenController::EnterFullscreenModeInternal(
FullscreenInternalOption option) {
toggled_into_fullscreen_ = true;
GURL url;
if (option == TAB) {
url = GetRequestingOrigin();
tab_fullscreen_accepted_ = GetFullscreenSetting() == CONTENT_SETTING_ALLOW;
} else {
if (!extension_caused_fullscreen_.is_empty())
url = extension_caused_fullscreen_;
}
if (option == BROWSER)
content::RecordAction(UserMetricsAction("ToggleFullscreen"));
// TODO(scheib): Record metrics for WITH_TOOLBAR, without counting transitions
// from tab fullscreen out to browser with toolbar.
exclusive_access_manager()->context()->EnterFullscreen(
url, exclusive_access_manager()->GetExclusiveAccessExitBubbleType(),
option == BROWSER_WITH_TOOLBAR);
exclusive_access_manager()->UpdateExclusiveAccessExitBubbleContent();
// Once the window has become fullscreen it'll call back to
// WindowFullscreenStateChanged(). We don't do this immediately as
// BrowserWindow::EnterFullscreen() asks for bookmark_bar_state_, so we let
// the BrowserWindow invoke WindowFullscreenStateChanged when appropriate.
}
void FullscreenController::ExitFullscreenModeInternal() {
toggled_into_fullscreen_ = false;
#if defined(OS_MACOSX)
// Mac windows report a state change instantly, and so we must also clear
// state_prior_to_tab_fullscreen_ to match them else other logic using
// state_prior_to_tab_fullscreen_ will be incorrect.
NotifyTabExclusiveAccessLost();
#endif
exclusive_access_manager()->context()->ExitFullscreen();
extension_caused_fullscreen_ = GURL();
exclusive_access_manager()->UpdateExclusiveAccessExitBubbleContent();
}
ContentSetting FullscreenController::GetFullscreenSetting() const {
DCHECK(exclusive_access_tab());
GURL url = GetRequestingOrigin();
// Always ask on file:// URLs, since we can't meaningfully make the
// decision stick for a particular origin.
// TODO(estark): Revisit this when crbug.com/455882 is fixed.
if (url.SchemeIsFile())
return CONTENT_SETTING_ASK;
if (IsPrivilegedFullscreenForTab())
return CONTENT_SETTING_ALLOW;
// If the permission was granted to the website with no embedder, it should
// always be allowed, even if embedded.
if (exclusive_access_manager()
->context()
->GetProfile()
->GetHostContentSettingsMap()
->GetContentSetting(url, url, CONTENT_SETTINGS_TYPE_FULLSCREEN,
std::string()) == CONTENT_SETTING_ALLOW) {
return CONTENT_SETTING_ALLOW;
}
// See the comment above the call to |SetContentSetting()| for how the
// requesting and embedding origins interact with each other wrt permissions.
return exclusive_access_manager()
->context()
->GetProfile()
->GetHostContentSettingsMap()
->GetContentSetting(url, GetEmbeddingOrigin(),
CONTENT_SETTINGS_TYPE_FULLSCREEN, std::string());
}
bool FullscreenController::IsPrivilegedFullscreenForTab() const {
const bool embedded_widget_present =
exclusive_access_tab() &&
exclusive_access_tab()->GetFullscreenRenderWidgetHostView();
return embedded_widget_present || is_privileged_fullscreen_for_testing_;
}
void FullscreenController::SetPrivilegedFullscreenForTesting(
bool is_privileged) {
is_privileged_fullscreen_for_testing_ = is_privileged;
}
bool FullscreenController::MaybeToggleFullscreenForCapturedTab(
WebContents* web_contents, bool enter_fullscreen) {
if (enter_fullscreen) {
if (web_contents->GetCapturerCount() > 0) {
FullscreenWithinTabHelper::CreateForWebContents(web_contents);
FullscreenWithinTabHelper::FromWebContents(web_contents)->
SetIsFullscreenForCapturedTab(true);
return true;
}
} else {
if (IsFullscreenForCapturedTab(web_contents)) {
FullscreenWithinTabHelper::RemoveForWebContents(web_contents);
return true;
}
}
return false;
}
bool FullscreenController::IsFullscreenForCapturedTab(
const WebContents* web_contents) const {
// Note: On Mac, some of the OnTabXXX() methods get called with a nullptr
// value
// for web_contents. Check for that here.
const FullscreenWithinTabHelper* const helper =
web_contents ? FullscreenWithinTabHelper::FromWebContents(web_contents)
: nullptr;
if (helper && helper->is_fullscreen_for_captured_tab()) {
DCHECK_NE(exclusive_access_tab(), web_contents);
return true;
}
return false;
}
GURL FullscreenController::GetRequestingOrigin() const {
DCHECK(exclusive_access_tab());
if (!fullscreened_origin_.is_empty())
return fullscreened_origin_;
return exclusive_access_tab()->GetLastCommittedURL();
}
GURL FullscreenController::GetEmbeddingOrigin() const {
DCHECK(exclusive_access_tab());
return exclusive_access_tab()->GetLastCommittedURL();
}