| // 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 |