blob: 240541a8377cab4f2c689178228b1b2a2ae243c1 [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/web_app_launch_utils.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/trace_event/base_tracing.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/app_mode/app_mode_utils.h"
#include "chrome/browser/buildflags.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/app_session_service.h"
#include "chrome/browser/sessions/app_session_service_factory.h"
#include "chrome/browser/sessions/session_service_base.h"
#include "chrome/browser/sessions/session_service_factory.h"
#include "chrome/browser/sessions/session_service_lookup.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.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/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/browser/web_applications/web_app_tab_helper.h"
#include "chrome/common/chrome_features.h"
#include "components/omnibox/browser/location_bar_model.h"
#include "components/site_engagement/content/site_engagement_service.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "extensions/common/constants.h"
#include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h"
#include "url/gurl.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "chrome/browser/ui/extensions/hosted_app_browser_controller.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/extension.h"
#endif
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "components/user_manager/user_manager.h"
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
namespace web_app {
namespace {
ui::WindowShowState DetermineWindowShowState() {
if (chrome::IsRunningInForcedAppMode())
return ui::SHOW_STATE_FULLSCREEN;
return ui::SHOW_STATE_DEFAULT;
}
Browser* ReparentWebContentsIntoAppBrowser(content::WebContents* contents,
Browser* target_browser) {
DCHECK(target_browser->is_type_app());
Browser* source_browser = chrome::FindBrowserWithWebContents(contents);
// In a reparent, the owning session service needs to be told it's tab
// has been removed, otherwise it will reopen the tab on restoration.
SessionServiceBase* service =
GetAppropriateSessionServiceForProfile(source_browser);
service->TabClosing(contents);
TabStripModel* source_tabstrip = source_browser->tab_strip_model();
// Avoid causing the existing browser window to close if this is the last tab
// remaining.
if (source_tabstrip->count() == 1)
chrome::NewTab(source_browser);
target_browser->tab_strip_model()->AppendWebContents(
source_tabstrip->DetachWebContentsAtForInsertion(
source_tabstrip->GetIndexOfWebContents(contents)),
true);
target_browser->window()->Show();
// The app window will be registered correctly, however the tab will not
// be correctly tracked. We need to do a reset to get the tab correctly
// tracked by the app service.
AppSessionService* app_service =
AppSessionServiceFactory::GetForProfile(target_browser->profile());
app_service->ResetFromCurrentBrowsers();
return target_browser;
}
} // namespace
absl::optional<AppId> GetWebAppForActiveTab(Browser* browser) {
WebAppProvider* provider = WebAppProvider::GetForWebApps(browser->profile());
if (!provider)
return absl::nullopt;
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
if (!web_contents)
return absl::nullopt;
return provider->registrar().FindInstalledAppWithUrlInScope(
web_contents->GetMainFrame()->GetLastCommittedURL());
}
bool IsInScope(const GURL& url, const GURL& scope) {
return base::StartsWith(url.spec(), scope.spec(),
base::CompareCase::SENSITIVE);
}
void PrunePreScopeNavigationHistory(const GURL& scope,
content::WebContents* contents) {
content::NavigationController& navigation_controller =
contents->GetController();
if (!navigation_controller.CanPruneAllButLastCommitted())
return;
int index = navigation_controller.GetEntryCount() - 1;
while (index >= 0 &&
IsInScope(navigation_controller.GetEntryAtIndex(index)->GetURL(),
scope)) {
--index;
}
while (index >= 0) {
navigation_controller.RemoveEntryAtIndex(index);
--index;
}
}
Browser* ReparentWebAppForActiveTab(Browser* browser) {
absl::optional<AppId> app_id = GetWebAppForActiveTab(browser);
if (!app_id)
return nullptr;
return ReparentWebContentsIntoAppBrowser(
browser->tab_strip_model()->GetActiveWebContents(), *app_id);
}
Browser* ReparentWebContentsIntoAppBrowser(content::WebContents* contents,
const AppId& app_id) {
Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
// Incognito tabs reparent correctly, but remain incognito without any
// indication to the user, so disallow it.
DCHECK(!profile->IsOffTheRecord());
// Clear navigation history that occurred before the user most recently
// entered the app's scope. The minimal-ui Back button will be initially
// disabled if the previous page was outside scope. Packaged apps are not
// affected.
WebAppRegistrar& registrar =
WebAppProvider::GetForWebApps(profile)->registrar();
if (registrar.IsInstalled(app_id)) {
absl::optional<GURL> app_scope = registrar.GetAppScope(app_id);
if (!app_scope)
app_scope = registrar.GetAppStartUrl(app_id).GetWithoutFilename();
PrunePreScopeNavigationHistory(*app_scope, contents);
}
auto launch_url = contents->GetLastCommittedURL();
RecordMetrics(app_id, apps::mojom::LaunchContainer::kLaunchContainerWindow,
extensions::AppLaunchSource::kSourceReparenting, launch_url,
contents);
if (registrar.IsTabbedWindowModeEnabled(app_id)) {
for (Browser* browser : *BrowserList::GetInstance()) {
if (AppBrowserController::IsForWebApp(browser, app_id))
return ReparentWebContentsIntoAppBrowser(contents, browser);
}
}
return ReparentWebContentsIntoAppBrowser(
contents,
Browser::Create(Browser::CreateParams::CreateForApp(
GenerateApplicationNameFromAppId(app_id), true /* trusted_source */,
gfx::Rect(), profile, true /* user_gesture */)));
}
void SetWebContentsActingAsApp(content::WebContents* contents,
const AppId& app_id) {
auto* helper = WebAppTabHelper::FromWebContents(contents);
helper->SetAppId(app_id);
helper->set_acting_as_app(true);
}
void SetAppPrefsForWebContents(content::WebContents* web_contents) {
web_contents->GetMutableRendererPrefs()->can_accept_load_drops = false;
web_contents->SyncRendererPrefs();
web_contents->NotifyPreferencesChanged();
}
void ClearAppPrefsForWebContents(content::WebContents* web_contents) {
web_contents->GetMutableRendererPrefs()->can_accept_load_drops = true;
web_contents->SyncRendererPrefs();
web_contents->NotifyPreferencesChanged();
}
std::unique_ptr<AppBrowserController> MaybeCreateAppBrowserController(
Browser* browser) {
std::unique_ptr<AppBrowserController> controller;
const AppId app_id = GetAppIdFromApplicationName(browser->app_name());
auto* const provider =
WebAppProvider::GetForLocalAppsUnchecked(browser->profile());
if (provider && provider->registrar().IsInstalled(app_id)) {
const SystemWebAppDelegate* system_app = nullptr;
auto system_app_type =
GetSystemWebAppTypeForAppId(browser->profile(), app_id);
if (system_app_type) {
system_app =
provider->system_web_app_manager().GetSystemApp(*system_app_type);
}
const bool has_tab_strip =
!browser->is_type_app_popup() &&
((system_app && system_app->ShouldHaveTabStrip()) ||
provider->registrar().IsTabbedWindowModeEnabled(app_id));
controller = std::make_unique<WebAppBrowserController>(
*provider, browser, app_id, system_app, has_tab_strip);
} else {
#if BUILDFLAG(ENABLE_EXTENSIONS)
const extensions::Extension* extension =
extensions::ExtensionRegistry::Get(browser->profile())
->GetExtensionById(app_id,
extensions::ExtensionRegistry::EVERYTHING);
if (extension && extension->is_hosted_app()) {
controller =
std::make_unique<extensions::HostedAppBrowserController>(browser);
}
#endif
}
if (controller)
controller->Init();
return controller;
}
Browser* CreateWebApplicationWindow(Profile* profile,
const std::string& app_id,
WindowOpenDisposition disposition,
int32_t restore_id,
bool omit_from_session_restore,
bool can_resize,
bool can_maximize,
const gfx::Rect initial_bounds) {
std::string app_name = GenerateApplicationNameFromAppId(app_id);
Browser::CreateParams browser_params =
disposition == WindowOpenDisposition::NEW_POPUP
? Browser::CreateParams::CreateForAppPopup(
app_name, /*trusted_source=*/true, initial_bounds, profile,
/*user_gesture=*/true)
: Browser::CreateParams::CreateForApp(
app_name, /*trusted_source=*/true, initial_bounds, profile,
/*user_gesture=*/true);
browser_params.initial_show_state = DetermineWindowShowState();
#if BUILDFLAG(IS_CHROMEOS_ASH)
browser_params.restore_id = restore_id;
#endif
browser_params.omit_from_session_restore = omit_from_session_restore;
browser_params.can_resize = can_resize;
browser_params.can_maximize = can_maximize;
browser_params.are_tab_groups_enabled = false;
return Browser::Create(browser_params);
}
content::WebContents* NavigateWebApplicationWindow(
Browser* browser,
const std::string& app_id,
const GURL& url,
WindowOpenDisposition disposition) {
NavigateParams nav_params(browser, url, ui::PAGE_TRANSITION_AUTO_BOOKMARK);
nav_params.disposition = disposition;
return NavigateWebAppUsingParams(app_id, nav_params);
}
content::WebContents* NavigateWebAppUsingParams(const std::string& app_id,
NavigateParams& nav_params) {
Browser* browser = nav_params.browser;
const absl::optional<SystemAppType> capturing_system_app_type =
GetCapturingSystemAppForURL(browser->profile(), nav_params.url);
// TODO(crbug.com/1201820): This block creates conditions where Navigate()
// returns early and causes a crash. Fail gracefully instead. Further
// debugging state will be implemented via Chrometto UMA traces.
if (capturing_system_app_type &&
(!browser ||
!IsBrowserForSystemWebApp(browser, capturing_system_app_type.value()))) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
auto* user_manager = user_manager::UserManager::Get();
bool is_kiosk = user_manager && user_manager->IsLoggedInAsAnyKioskApp();
AppBrowserController* app_controller = browser->app_controller();
WebAppProvider* web_app_provider =
WebAppProvider::GetForLocalAppsUnchecked(browser->profile());
TRACE_EVENT_INSTANT(
"system_apps", "BadNavigate", [&](perfetto::EventContext ctx) {
auto* bad_navigate =
ctx.event<perfetto::protos::pbzero::ChromeTrackEvent>()
->set_chrome_web_app_bad_navigate();
bad_navigate->set_is_kiosk(is_kiosk);
bad_navigate->set_has_hosted_app_controller(!!app_controller);
bad_navigate->set_app_name(browser->app_name());
if (app_controller && app_controller->system_app()) {
bad_navigate->set_system_app_type(
static_cast<uint32_t>(app_controller->system_app()->GetType()));
}
bad_navigate->set_web_app_provider_registry_ready(
web_app_provider->on_registry_ready().is_signaled());
bad_navigate->set_system_web_app_manager_synchronized(
web_app_provider->system_web_app_manager()
.on_apps_synchronized()
.is_signaled());
});
UMA_HISTOGRAM_ENUMERATION("WebApp.SystemApps.BadNavigate.Type",
capturing_system_app_type.value());
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
return nullptr;
}
Navigate(&nav_params);
content::WebContents* const web_contents =
nav_params.navigated_or_inserted_contents;
if (web_contents) {
SetWebContentsActingAsApp(web_contents, app_id);
SetAppPrefsForWebContents(web_contents);
}
return web_contents;
}
void RecordAppWindowLaunch(Profile* profile, const std::string& app_id) {
WebAppProvider* provider = WebAppProvider::GetForLocalAppsUnchecked(profile);
if (!provider)
return;
DisplayMode display =
provider->registrar().GetEffectiveDisplayModeFromManifest(app_id);
if (display == DisplayMode::kUndefined)
return;
DCHECK_LT(DisplayMode::kUndefined, display);
DCHECK_LE(display, DisplayMode::kMaxValue);
UMA_HISTOGRAM_ENUMERATION("Launch.WebAppDisplayMode", display);
}
void RecordMetrics(const AppId& app_id,
apps::mojom::LaunchContainer container,
extensions::AppLaunchSource launch_source,
const GURL& launch_url,
content::WebContents* web_contents) {
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
// TODO(crbug.com/1014328): Populate WebApp metrics instead of Extensions.
if (container == apps::mojom::LaunchContainer::kLaunchContainerTab) {
UMA_HISTOGRAM_ENUMERATION("Extensions.AppTabLaunchType",
extensions::LAUNCH_TYPE_REGULAR, 100);
} else if (container ==
apps::mojom::LaunchContainer::kLaunchContainerWindow) {
RecordAppWindowLaunch(profile, app_id);
}
UMA_HISTOGRAM_ENUMERATION("Extensions.BookmarkAppLaunchSource",
launch_source);
UMA_HISTOGRAM_ENUMERATION("Extensions.BookmarkAppLaunchContainer", container);
// Record the launch time in the site engagement service. A recent web
// app launch will provide an engagement boost to the origin.
site_engagement::SiteEngagementService::Get(profile)
->SetLastShortcutLaunchTime(web_contents, launch_url);
WebAppProvider::GetForWebApps(profile)->sync_bridge().SetAppLastLaunchTime(
app_id, base::Time::Now());
// Refresh the app banner added to homescreen event. The user may have
// cleared their browsing data since installing the app, which removes the
// event and will potentially permit a banner to be shown for the site.
RecordAppBanner(web_contents, launch_url);
}
} // namespace web_app