blob: 10e2c8b17e141609a7b4a410ecbcbc3a319a8f67 [file] [log] [blame]
// Copyright 2019 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/web_applications/app_browser_controller.h"
#include "base/bind.h"
#include "base/feature_list.h"
#include "base/strings/string_piece.h"
#include "chrome/browser/installable/installable_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/security_state_tab_helper.h"
#include "chrome/browser/themes/browser_theme_pack.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window_state.h"
#include "chrome/browser/ui/extensions/hosted_app_browser_controller.h"
#include "chrome/browser/ui/manifest_web_app_browser_controller.h"
#include "chrome/browser/ui/tabs/tab_menu_model_factory.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/web_applications/system_web_app_ui_utils.h"
#include "chrome/browser/ui/web_applications/web_app_browser_controller.h"
#include "chrome/browser/web_applications/components/app_registrar.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/browser/web_applications/system_web_app_manager.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/themes/autogenerated_theme_util.h"
#include "chrome/grit/generated_resources.h"
#include "components/security_state/core/security_state.h"
#include "components/url_formatter/url_formatter.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/constants.h"
#include "net/base/escape.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/image/image_skia.h"
#include "url/gurl.h"
#if defined(OS_CHROMEOS)
#include "chrome/browser/chromeos/crostini/crostini_terminal.h"
#endif
namespace {
class TerminalTabMenuModel : public ui::SimpleMenuModel {
public:
explicit TerminalTabMenuModel(ui::SimpleMenuModel::Delegate* delegate)
: ui::SimpleMenuModel(delegate) {
AddItemWithStringId(TabStripModel::CommandNewTabToRight,
IDS_TAB_CXMENU_NEWTABTORIGHT);
AddSeparator(ui::NORMAL_SEPARATOR);
AddItemWithStringId(TabStripModel::CommandCloseTab,
IDS_TAB_CXMENU_CLOSETAB);
AddItemWithStringId(TabStripModel::CommandCloseOtherTabs,
IDS_TAB_CXMENU_CLOSEOTHERTABS);
AddItemWithStringId(TabStripModel::CommandCloseTabsToRight,
IDS_TAB_CXMENU_CLOSETABSTORIGHT);
}
};
class TerminalTabMenuModelFactory : public TabMenuModelFactory {
public:
std::unique_ptr<ui::SimpleMenuModel> Create(
ui::SimpleMenuModel::Delegate* delegate,
TabStripModel*,
int) override {
return std::make_unique<TerminalTabMenuModel>(delegate);
}
};
} // namespace
namespace web_app {
namespace {
constexpr gfx::Rect TERMINAL_DEFAULT_BOUNDS(gfx::Point(64, 64),
gfx::Size(652, 484));
constexpr gfx::Size TERMINAL_SETTINGS_DEFAULT_SIZE(768, 512);
} // namespace
// static
std::unique_ptr<AppBrowserController>
AppBrowserController::MaybeCreateWebAppController(Browser* browser) {
#if BUILDFLAG(ENABLE_EXTENSIONS)
const AppId app_id = GetAppIdFromApplicationName(browser->app_name());
if (base::FeatureList::IsEnabled(features::kDesktopPWAsWithoutExtensions)) {
auto* provider = WebAppProvider::Get(browser->profile());
if (provider && provider->registrar().IsInstalled(app_id))
return std::make_unique<WebAppBrowserController>(browser);
}
const extensions::Extension* extension =
extensions::ExtensionRegistry::Get(browser->profile())
->GetExtensionById(app_id, extensions::ExtensionRegistry::EVERYTHING);
if (extension && extension->is_hosted_app()) {
if (base::FeatureList::IsEnabled(
features::kDesktopPWAsUnifiedUiController) &&
extension->from_bookmark()) {
return std::make_unique<WebAppBrowserController>(browser);
}
return std::make_unique<extensions::HostedAppBrowserController>(browser);
}
#endif
if (browser->is_focus_mode())
return std::make_unique<ManifestWebAppBrowserController>(browser);
return nullptr;
}
// static
bool AppBrowserController::IsForWebAppBrowser(const Browser* browser) {
return browser && browser->app_controller();
}
// static
base::string16 AppBrowserController::FormatUrlOrigin(const GURL& url) {
return url_formatter::FormatUrl(
url.GetOrigin(),
url_formatter::kFormatUrlOmitUsernamePassword |
url_formatter::kFormatUrlOmitHTTPS |
url_formatter::kFormatUrlOmitHTTP |
url_formatter::kFormatUrlOmitTrailingSlashOnBareHostname |
url_formatter::kFormatUrlOmitTrivialSubdomains,
net::UnescapeRule::SPACES, nullptr, nullptr, nullptr);
}
const ui::ThemeProvider* AppBrowserController::GetThemeProvider() const {
return theme_provider_.get();
}
AppBrowserController::AppBrowserController(
Browser* browser,
base::Optional<web_app::AppId> app_id)
: content::WebContentsObserver(nullptr),
app_id_(std::move(app_id)),
browser_(browser),
theme_provider_(
ThemeService::CreateBoundThemeProvider(browser_->profile(), this)),
system_app_type_(HasAppId() ? WebAppProvider::Get(browser->profile())
->system_web_app_manager()
.GetSystemAppTypeForAppId(GetAppId())
: base::nullopt),
// TODO(crbug.com/1061822): Generalise has_tab_strip_ as a SystemWebApp
// capability.
has_tab_strip_(
system_app_type_ == SystemAppType::TERMINAL ||
(base::FeatureList::IsEnabled(features::kDesktopPWAsTabStrip) &&
HasAppId() &&
WebAppProvider::Get(browser->profile())
->registrar()
.IsInExperimentalTabbedWindowMode(GetAppId()))) {
browser->tab_strip_model()->AddObserver(this);
UpdateThemePack();
}
AppBrowserController::~AppBrowserController() {
browser()->tab_strip_model()->RemoveObserver(this);
}
bool AppBrowserController::ShouldShowCustomTabBar() const {
if (!IsInstalled())
return false;
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
if (!web_contents)
return false;
GURL launch_url = GetAppLaunchURL();
base::StringPiece launch_scheme = launch_url.scheme_piece();
bool is_internal_launch_scheme =
launch_scheme == extensions::kExtensionScheme ||
launch_scheme == content::kChromeUIScheme ||
launch_scheme == content::kChromeUIUntrustedScheme;
// The current page must be secure for us to hide the toolbar. However,
// chrome:// launch URL apps can hide the toolbar,
// if the current WebContents URLs are the same as the launch scheme.
//
// Note that the launch scheme may be insecure, but as long as the current
// page's scheme is secure, we can hide the toolbar.
base::StringPiece secure_page_scheme =
is_internal_launch_scheme ? launch_scheme : url::kHttpsScheme;
auto should_show_toolbar_for_url = [&](const GURL& url) -> bool {
// If the url is unset, it doesn't give a signal as to whether the toolbar
// should be shown or not. In lieu of more information, do not show the
// toolbar.
if (url.is_empty())
return false;
// Page URLs that are not within scope
// (https://www.w3.org/TR/appmanifest/#dfn-within-scope) of the app
// corresponding to |launch_url| show the toolbar.
bool out_of_scope = !IsUrlInAppScope(url);
if (url.scheme_piece() != secure_page_scheme) {
// Some origins are (such as localhost) are considered secure even when
// served over non-secure schemes. However, in order to hide the toolbar,
// the 'considered secure' origin must also be in the app's scope.
return out_of_scope || !InstallableManager::IsOriginConsideredSecure(url);
}
if (is_for_system_web_app()) {
DCHECK(url.scheme_piece() == content::kChromeUIScheme ||
url.scheme_piece() == content::kChromeUIUntrustedScheme);
return false;
}
return out_of_scope;
};
GURL visible_url = web_contents->GetVisibleURL();
GURL last_committed_url = web_contents->GetLastCommittedURL();
if (last_committed_url.is_empty() && visible_url.is_empty())
return should_show_toolbar_for_url(initial_url());
if (should_show_toolbar_for_url(visible_url) ||
should_show_toolbar_for_url(last_committed_url)) {
return true;
}
// Insecure external web sites show the toolbar.
// Note: IsContentSecure is false until a navigation is committed.
if (!last_committed_url.is_empty() && !is_internal_launch_scheme &&
!InstallableManager::IsContentSecure(web_contents)) {
return true;
}
return false;
}
bool AppBrowserController::has_tab_strip() const {
return has_tab_strip_;
}
bool AppBrowserController::HasTitlebarMenuButton() const {
// Hide for system apps.
return !is_for_system_web_app();
}
bool AppBrowserController::HasTitlebarAppOriginText() const {
// Do not show origin text for System Apps.
return !is_for_system_web_app();
}
bool AppBrowserController::HasTitlebarContentSettings() const {
// Do not show content settings for System Apps.
return !is_for_system_web_app();
}
bool AppBrowserController::IsInstalled() const {
return false;
}
std::unique_ptr<TabMenuModelFactory>
AppBrowserController::GetTabMenuModelFactory() const {
if (system_app_type_ == SystemAppType::TERMINAL) {
// TODO(crbug.com/1061822) move terminal specific code out.
return std::make_unique<TerminalTabMenuModelFactory>();
}
return nullptr;
}
bool AppBrowserController::IsHostedApp() const {
return false;
}
WebAppBrowserController* AppBrowserController::AsWebAppBrowserController() {
return nullptr;
}
bool AppBrowserController::CanUninstall() const {
return false;
}
void AppBrowserController::Uninstall() {
NOTREACHED();
return;
}
void AppBrowserController::UpdateCustomTabBarVisibility(bool animate) const {
browser()->window()->UpdateCustomTabBarVisibility(ShouldShowCustomTabBar(),
animate);
}
gfx::Rect AppBrowserController::GetDefaultBounds() const {
// TODO(crbug.com/1061822): Generalise default bounds as a SystemWebApp
// capability.
if (system_app_type_ == SystemAppType::TERMINAL) {
// Terminal settings is centered.
if (browser()->is_type_app_popup()) {
gfx::Rect bounds =
display::Screen::GetScreen()->GetDisplayForNewWindows().work_area();
bounds.ClampToCenteredSize(TERMINAL_SETTINGS_DEFAULT_SIZE);
return bounds;
}
return TERMINAL_DEFAULT_BOUNDS;
}
return gfx::Rect();
}
bool AppBrowserController::ShouldShowTabContextMenuShortcut(
int command_id) const {
#if defined(OS_CHROMEOS)
// TODO(crbug.com/1061822): Generalize ShouldShowTabContextMenuShortcut as
// a SystemWebApp capability.
if (system_app_type_ == SystemAppType::TERMINAL &&
command_id == TabStripModel::CommandCloseTab) {
return crostini::GetTerminalSettingPassCtrlW(browser()->profile());
}
#endif
return true;
}
void AppBrowserController::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
if (!initial_url().is_empty())
return;
if (!navigation_handle->IsInMainFrame())
return;
if (navigation_handle->GetURL().is_empty())
return;
SetInitialURL(navigation_handle->GetURL());
}
void AppBrowserController::DOMContentLoaded(
content::RenderFrameHost* render_frame_host) {
// We hold off changing theme color for a new tab until the page is loaded.
DidChangeThemeColor();
}
void AppBrowserController::DidChangeThemeColor() {
base::Optional<SkColor> theme_color = GetThemeColor();
if (theme_color == last_theme_color_)
return;
last_theme_color_ = theme_color;
UpdateThemePack();
browser_->window()->UpdateFrameColor();
if (has_tab_strip_) {
// TODO(crbug.com/1020050): Add separate change type for this situation, on
// Windows this causes the frame to be recreated which is visually
// disruptive.
browser_->window()->UserChangedTheme(BrowserThemeChangeType::kBrowserTheme);
}
}
base::Optional<SkColor> AppBrowserController::GetThemeColor() const {
base::Optional<SkColor> result;
// HTML meta theme-color tag overrides manifest theme_color, see spec:
// https://www.w3.org/TR/appmanifest/#theme_color-member
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
if (web_contents) {
base::Optional<SkColor> color = web_contents->GetThemeColor();
if (color)
result = color;
}
if (!result)
return base::nullopt;
// The frame/tabstrip code expects an opaque color.
return SkColorSetA(*result, SK_AlphaOPAQUE);
}
base::string16 AppBrowserController::GetTitle() const {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
if (!web_contents)
return base::string16();
content::NavigationEntry* entry =
web_contents->GetController().GetVisibleEntry();
return entry ? entry->GetTitle() : base::string16();
}
void AppBrowserController::OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
if (selection.active_tab_changed()) {
content::WebContentsObserver::Observe(selection.new_contents);
// Update themes when we switch tabs, or create the first tab, but not
// when we create 2nd or subsequent tabs. They should keep current theme
// until page loads. See |DOMContentLoaded|.
if (change.type() != TabStripModelChange::kInserted ||
tab_strip_model->count() == 1) {
DidChangeThemeColor();
}
}
if (change.type() == TabStripModelChange::kInserted) {
for (const auto& contents : change.GetInsert()->contents)
OnTabInserted(contents.contents);
} else if (change.type() == TabStripModelChange::kRemoved) {
for (const auto& contents : change.GetRemove()->contents)
OnTabRemoved(contents.contents);
// WebContents should be null when the last tab is closed.
DCHECK_EQ(web_contents() == nullptr, tab_strip_model->empty());
}
UpdateCustomTabBarVisibility(/*animate=*/false);
}
CustomThemeSupplier* AppBrowserController::GetThemeSupplier() const {
return theme_pack_.get();
}
void AppBrowserController::OnReceivedInitialURL() {
UpdateCustomTabBarVisibility(/*animate=*/false);
// If the window bounds have not been overridden, there is no need to resize
// the window.
if (!browser()->bounds_overridden())
return;
// The saved bounds will only be wrong if they are content bounds.
if (!chrome::SavedBoundsAreContentBounds(browser()))
return;
// TODO(crbug.com/964825): Correctly set the window size at creation time.
// This is currently not possible because the current url is not easily known
// at popup construction time.
browser()->window()->SetContentsSize(browser()->override_bounds().size());
}
void AppBrowserController::OnTabInserted(content::WebContents* contents) {
if (!contents->GetVisibleURL().is_empty() && initial_url_.is_empty())
SetInitialURL(contents->GetVisibleURL());
}
void AppBrowserController::OnTabRemoved(content::WebContents* contents) {}
gfx::ImageSkia AppBrowserController::GetFallbackAppIcon() const {
gfx::ImageSkia page_icon = browser()->GetCurrentPageIcon().AsImageSkia();
if (!page_icon.isNull())
return page_icon;
// The icon may be loading still. Return a transparent icon rather
// than using a placeholder to avoid flickering.
SkBitmap bitmap;
bitmap.allocN32Pixels(gfx::kFaviconSize, gfx::kFaviconSize);
bitmap.eraseColor(SK_ColorTRANSPARENT);
return gfx::ImageSkia::CreateFrom1xBitmap(bitmap);
}
void AppBrowserController::SetInitialURL(const GURL& initial_url) {
DCHECK(initial_url_.is_empty());
initial_url_ = initial_url;
OnReceivedInitialURL();
}
void AppBrowserController::UpdateThemePack() {
base::Optional<SkColor> theme_color = GetThemeColor();
if (!theme_color) {
theme_pack_ = nullptr;
return;
}
theme_pack_ = base::MakeRefCounted<BrowserThemePack>(
CustomThemeSupplier::AUTOGENERATED);
AutogeneratedThemeColors colors;
SkColor theme_text_color = color_utils::GetColorWithMaxContrast(*theme_color);
if (has_tab_strip_) {
// AutogeneratedThemeColors will generally make the frame color match the
// theme color, but often adjusts it.
// We will use a similar approach to generate a main color and alt color
// with a 1.6 contrast ensuring that the active tab is exactly the theme.
colors.active_tab_color = *theme_color;
colors.active_tab_text_color = theme_text_color;
colors.frame_color = color_utils::BlendForMinContrast(
*theme_color, *theme_color, base::nullopt,
kAutogeneratedThemeActiveTabPreferredContrast)
.color;
colors.frame_text_color =
color_utils::GetColorWithMaxContrast(colors.frame_color);
} else {
// Set frame and active_tab to the same color when there are no tabs.
// Tab colors are used for tooltips and NTP background (bg shown until page
// loads).
// TODO(crbug.com/1053823): Add tests for theme properties being set in this
// branch.
colors.frame_color = *theme_color;
colors.frame_text_color = theme_text_color;
colors.active_tab_color = *theme_color;
colors.active_tab_text_color = theme_text_color;
}
BrowserThemePack::BuildFromColors(colors, theme_pack_.get());
}
} // namespace web_app