blob: 4dd6b852ef5bee23fba710c14878a57f11be4057 [file] [log] [blame]
// Copyright 2019 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/web_applications/app_browser_controller.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/strings/escape.h"
#include "base/strings/string_piece.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.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_properties.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window_state.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/tabs/tab_menu_model_factory.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.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 "components/webapps/browser/installable/installable_manager.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/common/constants.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/models/image_model.h"
#include "ui/color/color_id.h"
#include "ui/color/color_recipe.h"
#include "ui/color/color_transform.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/geometry/resize_utils.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/native_theme/native_theme.h"
#include "url/gurl.h"
#include "url/origin.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/apps/icon_standardizer.h"
#include "chrome/browser/ash/system_web_apps/types/system_web_app_delegate.h"
#include "chrome/browser/ash/system_web_apps/types/system_web_app_type.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#endif
namespace {
SkColor GetAltColor(SkColor color) {
return color_utils::BlendForMinContrast(
color, color, absl::nullopt,
kAutogeneratedThemeActiveTabPreferredContrast)
.color;
}
} // namespace
namespace web_app {
// static
bool AppBrowserController::IsWebApp(const Browser* browser) {
return browser && browser->app_controller();
}
// static
bool AppBrowserController::IsForWebApp(const Browser* browser,
const AppId& app_id) {
return IsWebApp(browser) && browser->app_controller()->app_id() == app_id;
}
// static
Browser* AppBrowserController::FindForWebApp(const Profile& profile,
const AppId& app_id) {
const BrowserList* browser_list = BrowserList::GetInstance();
for (auto it = browser_list->begin_browsers_ordered_by_activation();
it != browser_list->end_browsers_ordered_by_activation(); ++it) {
Browser* browser = *it;
if (browser->type() == Browser::TYPE_POPUP)
continue;
if (browser->profile() != &profile)
continue;
if (!IsForWebApp(browser, app_id))
continue;
return browser;
}
return nullptr;
}
// static
std::u16string AppBrowserController::FormatUrlOrigin(
const GURL& url,
url_formatter::FormatUrlTypes format_types) {
auto origin = url::Origin::Create(url);
return url_formatter::FormatUrl(origin.opaque() ? url : origin.GetURL(),
format_types, base::UnescapeRule::SPACES,
nullptr, nullptr, nullptr);
}
const ui::ThemeProvider* AppBrowserController::GetThemeProvider() const {
return theme_provider_.get();
}
AppBrowserController::AppBrowserController(
Browser* browser,
AppId app_id,
bool has_tab_strip)
: content::WebContentsObserver(nullptr),
browser_(browser),
app_id_(std::move(app_id)),
has_tab_strip_(has_tab_strip),
theme_provider_(
ThemeService::CreateBoundThemeProvider(browser_->profile(), this)) {
browser->tab_strip_model()->AddObserver(this);
}
AppBrowserController::AppBrowserController(Browser* browser, AppId app_id)
: AppBrowserController(browser, std::move(app_id), false) {}
void AppBrowserController::Init() {
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 start_url = GetAppStartUrl();
base::StringPiece start_url_scheme = start_url.scheme_piece();
bool is_internal_start_url_scheme =
start_url_scheme == extensions::kExtensionScheme ||
start_url_scheme == content::kChromeUIScheme ||
start_url_scheme == content::kChromeUIUntrustedScheme;
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;
// Show toolbar when not using 'https', unless this is an internal app,
// or origin is secure (e.g. localhost).
if (!is_internal_start_url_scheme && !url.SchemeIs(url::kHttpsScheme) &&
!webapps::InstallableManager::IsOriginConsideredSecure(url)) {
return true;
}
// Page URLs that are not within scope
// (https://www.w3.org/TR/appmanifest/#dfn-within-scope) of the app
// corresponding to |start_url| show the toolbar.
return !IsUrlInAppScope(url);
};
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_start_url_scheme &&
!webapps::InstallableManager::IsContentSecure(web_contents)) {
return true;
}
return false;
}
bool AppBrowserController::has_tab_strip() const {
return has_tab_strip_;
}
bool AppBrowserController::HasTitlebarMenuButton() const {
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Hide for system apps.
return !system_app();
#else
return true;
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
}
bool AppBrowserController::HasTitlebarAppOriginText() const {
bool hide = base::FeatureList::IsEnabled(features::kHideWebAppOriginText);
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Do not show origin text for System Apps.
if (system_app())
hide = true;
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
return !hide;
}
bool AppBrowserController::HasTitlebarContentSettings() const {
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Do not show content settings for System Apps.
return !system_app();
#else
return true;
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
}
std::vector<PageActionIconType> AppBrowserController::GetTitleBarPageActions()
const {
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (system_app()) {
return {PageActionIconType::kFind, PageActionIconType::kZoom};
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
std::vector<PageActionIconType> types_enabled;
types_enabled.push_back(PageActionIconType::kFind);
types_enabled.push_back(PageActionIconType::kManagePasswords);
types_enabled.push_back(PageActionIconType::kTranslate);
types_enabled.push_back(PageActionIconType::kZoom);
types_enabled.push_back(PageActionIconType::kFileSystemAccess);
types_enabled.push_back(PageActionIconType::kCookieControls);
types_enabled.push_back(PageActionIconType::kLocalCardMigration);
types_enabled.push_back(PageActionIconType::kSaveCard);
return types_enabled;
}
bool AppBrowserController::IsInstalled() const {
return false;
}
std::unique_ptr<TabMenuModelFactory>
AppBrowserController::GetTabMenuModelFactory() const {
return nullptr;
}
bool AppBrowserController::AppUsesWindowControlsOverlay() const {
return false;
}
bool AppBrowserController::AppUsesBorderlessMode() const {
return false;
}
bool AppBrowserController::AppUsesTabbed() const {
return false;
}
bool AppBrowserController::IsIsolatedWebApp() const {
return false;
}
bool AppBrowserController::IsWindowControlsOverlayEnabled() const {
return false;
}
void AppBrowserController::ToggleWindowControlsOverlayEnabled(
base::OnceClosure on_complete) {
std::move(on_complete).Run();
}
gfx::Rect AppBrowserController::GetDefaultBounds() const {
return gfx::Rect();
}
bool AppBrowserController::HasReloadButton() const {
return true;
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
const ash::SystemWebAppDelegate* AppBrowserController::system_app() const {
return nullptr;
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
std::u16string AppBrowserController::GetLaunchFlashText() const {
// Isolated Web Apps should show the app's name instead of the origin.
// App Short Name is considered trustworthy because manifest comes from signed
// web bundle.
// TODO:(crbug.com/b/1394199) Disable IWA launch flash text for OSs that
// already display name on title bar.
if (IsIsolatedWebApp()) {
return GetAppShortName();
}
return GetFormattedUrlOrigin();
}
bool AppBrowserController::IsHostedApp() const {
return false;
}
WebAppBrowserController* AppBrowserController::AsWebAppBrowserController() {
return nullptr;
}
bool AppBrowserController::CanUserUninstall() const {
return false;
}
void AppBrowserController::Uninstall(
webapps::WebappUninstallSource webapp_uninstall_source) {
NOTREACHED();
}
void AppBrowserController::UpdateCustomTabBarVisibility(bool animate) const {
browser()->window()->UpdateCustomTabBarVisibility(ShouldShowCustomTabBar(),
animate);
}
void AppBrowserController::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
if (!initial_url().is_empty())
return;
if (!navigation_handle->IsInPrimaryMainFrame())
return;
if (navigation_handle->GetURL().is_empty())
return;
SetInitialURL(navigation_handle->GetURL());
}
void AppBrowserController::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
if (!navigation_handle->IsInPrimaryMainFrame() ||
navigation_handle->IsSameDocument())
return;
// For borderless mode when we navigate out of scope and then back to scope,
// the draggable regions stay same and nothing triggers to re-initialize them.
// So if they are cleared, they don't work anymore when coming back to scope.
if (AppUsesBorderlessMode())
return;
// Reset the draggable regions so they are not cached on navigation.
draggable_region_ = absl::nullopt;
}
void AppBrowserController::DOMContentLoaded(
content::RenderFrameHost* render_frame_host) {
// We hold off changing theme color for a new tab until the page is loaded.
UpdateThemePack();
}
void AppBrowserController::DidChangeThemeColor() {
UpdateThemePack();
}
void AppBrowserController::OnBackgroundColorChanged() {
UpdateThemePack();
}
absl::optional<SkColor> AppBrowserController::GetThemeColor() const {
ui::NativeTheme* native_theme = ui::NativeTheme::GetInstanceForNativeUi();
if (native_theme->InForcedColorsMode()) {
// use system [Window ThemeColor] when enable high contrast
return native_theme->GetSystemThemeColor(
ui::NativeTheme::SystemThemeColor::kWindow);
}
absl::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) {
absl::optional<SkColor> color = web_contents->GetThemeColor();
if (color)
result = color;
}
if (!result)
return absl::nullopt;
// The frame/tabstrip code expects an opaque color.
return SkColorSetA(*result, SK_AlphaOPAQUE);
}
absl::optional<SkColor> AppBrowserController::GetBackgroundColor() const {
absl::optional<SkColor> color;
if (auto* web_contents = browser()->tab_strip_model()->GetActiveWebContents())
color = web_contents->GetBackgroundColor();
return color ? SkColorSetA(*color, SK_AlphaOPAQUE) : color;
}
std::u16string AppBrowserController::GetTitle() const {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
if (!web_contents)
return std::u16string();
content::NavigationEntry* entry =
web_contents->GetController().GetVisibleEntry();
return entry ? entry->GetTitle() : std::u16string();
}
std::string AppBrowserController::GetTitleForMediaControls() const {
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Only return the app name if we're a System Web App.
if (system_app())
return base::UTF16ToUTF8(GetAppShortName());
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
return std::string();
}
GURL AppBrowserController::GetAppNewTabUrl() const {
return GetAppStartUrl();
}
#if BUILDFLAG(IS_MAC)
bool AppBrowserController::AlwaysShowToolbarInFullscreen() const {
return true;
}
void AppBrowserController::ToggleAlwaysShowToolbarInFullscreen() {}
#endif
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 theme when tabs change unless there are no tabs, or if the tab has
// not finished loading, we will update later in DOMContentLoaded().
if (tab_strip_model->count() > 0 &&
selection.new_contents->IsDocumentOnLoadCompletedInPrimaryMainFrame()) {
UpdateThemePack();
}
}
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();
}
bool AppBrowserController::ShouldUseCustomFrame() const {
return true;
}
void AppBrowserController::AddColorMixers(
ui::ColorProvider* provider,
const ui::ColorProviderManager::Key& key) const {
constexpr SkAlpha kSeparatorOpacity = 0.15f * 255.0f;
#if !BUILDFLAG(IS_CHROMEOS_ASH)
// This color is the same as the default active frame color.
const absl::optional<SkColor> theme_color = GetThemeColor();
ui::ColorTransform default_background =
key.color_mode == ui::ColorProviderManager::ColorMode::kLight
? ui::ColorTransform(ui::kColorFrameActiveUnthemed)
: ui::HSLShift(ui::kColorFrameActiveUnthemed,
ThemeProperties::GetDefaultTint(
ThemeProperties::TINT_FRAME, true));
#endif
ui::ColorMixer& mixer = provider->AddMixer();
absl::optional<SkColor> bg_color = GetBackgroundColor();
// TODO(kylixrd): The definition of kColorPwaBackground isn't fully fleshed
// out yet. Whether or not the PWA background color is set is used in many
// locations to derive other colors. Those specific locations would need to be
// addressed in their own context.
if (bg_color)
mixer[kColorPwaBackground] = {bg_color.value()};
mixer[kColorPwaMenuButtonIcon] = {kColorToolbarButtonIcon};
mixer[kColorPwaSecurityChipForeground] = {ui::kColorSecondaryForeground};
mixer[kColorPwaSecurityChipForegroundDangerous] = {
ui::kColorAlertHighSeverity};
mixer[kColorPwaSecurityChipForegroundPolicyCert] = {
ui::kColorDisabledForeground};
mixer[kColorPwaSecurityChipForegroundSecure] = {
kColorPwaSecurityChipForeground};
auto separator_color =
ui::GetColorWithMaxContrast(kColorPwaToolbarBackground);
mixer[kColorPwaTabBarBottomSeparator] = ui::AlphaBlend(
separator_color, kColorPwaToolbarBackground, kSeparatorOpacity);
mixer[kColorPwaTabBarTopSeparator] =
ui::AlphaBlend(separator_color, kColorPwaTheme, kSeparatorOpacity);
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Ash system frames differ from ChromeOS browser frames.
mixer[kColorPwaTheme] = {chromeos::kDefaultFrameColor};
#else
mixer[kColorPwaTheme] = theme_color ? ui::ColorTransform(theme_color.value())
: default_background;
#endif
mixer[kColorPwaToolbarBackground] = {ui::kColorEndpointBackground};
mixer[kColorPwaToolbarButtonIcon] =
ui::DeriveDefaultIconColor(ui::kColorEndpointForeground);
mixer[kColorPwaToolbarButtonIconDisabled] =
ui::SetAlpha(kColorPwaToolbarButtonIcon, gfx::kDisabledControlAlpha);
if (bg_color)
mixer[kColorWebContentsBackground] = {kColorPwaBackground};
mixer[kColorInfoBarBackground] = {kColorPwaToolbarBackground};
mixer[kColorInfoBarForeground] = {kColorPwaToolbarButtonIcon};
}
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) {}
ui::ImageModel AppBrowserController::GetFallbackAppIcon() const {
gfx::ImageSkia page_icon = browser()->GetCurrentPageIcon().AsImageSkia();
if (!page_icon.isNull()) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
return ui::ImageModel::FromImageSkia(
apps::CreateStandardIconImage(page_icon));
#else
return ui::ImageModel::FromImageSkia(page_icon);
#endif
}
// 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 ui::ImageModel::FromImageSkia(
gfx::ImageSkia::CreateFrom1xBitmap(bitmap));
}
void AppBrowserController::UpdateDraggableRegion(const SkRegion& region) {
draggable_region_ = region;
if (on_draggable_region_set_for_testing_)
std::move(on_draggable_region_set_for_testing_).Run();
}
void AppBrowserController::SetOnUpdateDraggableRegionForTesting(
base::OnceClosure done) {
on_draggable_region_set_for_testing_ = std::move(done);
}
void AppBrowserController::UpdateThemePack() {
absl::optional<SkColor> theme_color = GetThemeColor();
AutogeneratedThemeColors colors;
// TODO(crbug.com/1053823): Add tests for theme properties being set in this
// branch.
absl::optional<SkColor> background_color = GetBackgroundColor();
if (theme_color == last_theme_color_ &&
background_color == last_background_color_) {
return;
}
last_theme_color_ = theme_color;
last_background_color_ = background_color;
bool no_custom_colors = !theme_color && !background_color;
bool non_tabbed_no_frame_color = !has_tab_strip_ && !theme_color;
if (no_custom_colors || non_tabbed_no_frame_color) {
theme_pack_ = nullptr;
if (browser_->window()) {
browser_->window()->UserChangedTheme(
BrowserThemeChangeType::kWebAppTheme);
}
return;
}
if (!theme_color) {
theme_color = GetAltColor(*background_color);
} else if (!background_color) {
background_color =
ui::NativeTheme::GetInstanceForNativeUi()->ShouldUseDarkColors()
? gfx::kGoogleGrey900
: SK_ColorWHITE;
}
// For regular web apps, frame gets theme color and active tab gets
// background color.
colors.frame_color = *theme_color;
colors.active_tab_color = *background_color;
colors.ntp_color = *background_color;
colors.frame_text_color =
color_utils::GetColorWithMaxContrast(colors.frame_color);
colors.active_tab_text_color =
color_utils::GetColorWithMaxContrast(colors.active_tab_color);
theme_pack_ = base::MakeRefCounted<BrowserThemePack>(
ui::ColorProviderManager::ThemeInitializerSupplier::ThemeType::
kAutogenerated);
BrowserThemePack::BuildFromColors(colors, theme_pack_.get());
if (browser_->window())
browser_->window()->UserChangedTheme(BrowserThemeChangeType::kWebAppTheme);
}
void AppBrowserController::SetInitialURL(const GURL& initial_url) {
DCHECK(initial_url_.is_empty());
initial_url_ = initial_url;
OnReceivedInitialURL();
}
} // namespace web_app