blob: 7c6a943f3bfd4e6f965919a5dbdec524a6e79774 [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_manager.h"
#include <string>
#include <utility>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/metrics/histogram_macros.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/trace_event/base_tracing.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/app_mode/app_mode_utils.h"
#include "chrome/browser/apps/app_service/app_launch_params.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.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/share_target_utils.h"
#include "chrome/browser/ui/web_applications/system_web_app_ui_utils.h"
#include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
#include "chrome/browser/web_applications/os_integration_manager.h"
#include "chrome/browser/web_applications/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_constants.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_install_utils.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/browser/web_launch/web_launch_files_helper.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_switches.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "components/site_engagement/content/site_engagement_service.h"
#include "components/webapps/browser/banners/app_banner_settings_helper.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/referrer.h"
#include "extensions/common/constants.h"
#include "third_party/blink/public/common/custom_handlers/protocol_handler_utils.h"
#include "ui/base/page_transition_types.h"
#include "ui/base/window_open_disposition.h"
#include "ui/display/scoped_display_for_new_windows.h"
#include "url/gurl.h"
#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;
}
void SetTabHelperAppId(content::WebContents* web_contents,
const std::string& app_id) {
// TODO(https://crbug.com/1032443):
// Eventually move this to browser_navigator.cc: CreateTargetContents().
WebAppTabHelper* tab_helper = WebAppTabHelper::FromWebContents(web_contents);
DCHECK(tab_helper);
tab_helper->SetAppId(app_id);
}
content::WebContents* NavigateWebAppUsingParams(const std::string& app_id,
NavigateParams& nav_params) {
Browser* browser = nav_params.browser;
const absl::optional<web_app::SystemAppType> capturing_system_app_type =
web_app::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 || !web_app::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) {
SetTabHelperAppId(web_contents, app_id);
web_app::SetAppPrefsForWebContents(web_contents);
}
return web_contents;
}
// TODO(crbug.com/1019239): Passing a WebAppProvider seems to be a bit of an
// anti-pattern. We should refactor this and other existing functions in this
// file to receive an OsIntegrationManager instead.
absl::optional<GURL> GetProtocolHandlingTranslatedUrl(
WebAppProvider& provider,
const apps::AppLaunchParams& params) {
if (!params.protocol_handler_launch_url.has_value())
return absl::nullopt;
GURL protocol_url(params.protocol_handler_launch_url.value());
if (!protocol_url.is_valid())
return absl::nullopt;
absl::optional<GURL> translated_url =
provider.os_integration_manager().TranslateProtocolUrl(params.app_id,
protocol_url);
return translated_url;
}
bool IsProtocolHandlerCommandLineArg(const base::CommandLine::StringType& arg) {
#if defined(OS_WIN)
GURL url(base::WideToUTF16(arg));
#else
GURL url(arg);
#endif
if (url.is_valid() && url.has_scheme()) {
bool has_custom_scheme_prefix = false;
return blink::IsValidCustomHandlerScheme(url.scheme(),
/* allow_ext_plus_prefix */ false,
has_custom_scheme_prefix);
}
return false;
}
bool DoesCommandLineContainProtocolUrl(const base::CommandLine& command_line) {
for (const auto& arg : command_line.GetArgs()) {
if (IsProtocolHandlerCommandLineArg(arg)) {
return true;
}
}
return false;
}
WebAppLaunchManager::OpenApplicationCallback&
GetOpenApplicationCallbackForTesting() {
static base::NoDestructor<WebAppLaunchManager::OpenApplicationCallback>
callback;
return *callback;
}
class LaunchProcess {
public:
LaunchProcess(Profile& profile, const apps::AppLaunchParams& params);
content::WebContents* Run();
private:
const apps::ShareTarget* MaybeGetShareTarget();
std::tuple<GURL, bool /*is_file_handling*/> GetLaunchUrl(
const apps::ShareTarget* share_target);
WindowOpenDisposition GetNavigationDisposition();
content::WebContents* MaybeLaunchSystemWebApp(const GURL& launch_url);
std::tuple<Browser*, WindowOpenDisposition> EnsureBrowser();
Browser* MaybeFindBrowserForLaunch();
Browser* CreateBrowserForLaunch();
content::WebContents* NavigateBrowser(
Browser* browser,
const GURL& launch_url,
WindowOpenDisposition navigation_disposition,
const apps::ShareTarget* share_target);
void MaybeEnqueueWebLaunchParams(const GURL& launch_url,
bool is_file_handling,
content::WebContents* web_contents);
void RecordMetrics(const GURL& launch_url,
content::WebContents* web_contents);
Profile& profile_;
WebAppProvider& provider_;
const apps::AppLaunchParams& params_;
};
LaunchProcess::LaunchProcess(Profile& profile,
const apps::AppLaunchParams& params)
: profile_(profile),
provider_(*WebAppProvider::GetForLocalAppsUnchecked(&profile)),
params_(params) {}
content::WebContents* LaunchProcess::Run() {
if (Browser::GetCreationStatusForProfile(&profile_) !=
Browser::CreationStatus::kOk ||
!provider_.registrar().IsInstalled(params_.app_id)) {
return nullptr;
}
// Place new windows on the specified display.
display::ScopedDisplayForNewWindows scoped_display(params_.display_id);
const apps::ShareTarget* share_target = MaybeGetShareTarget();
GURL launch_url;
bool is_file_handling = false;
std::tie(launch_url, is_file_handling) = GetLaunchUrl(share_target);
// System Web Apps have their own launch code path.
// TODO(crbug.com/1231886): Don't use a separate code path so that SWAs can
// maintain feature parity with regular web apps (e.g. launch_handler
// behaviours).
content::WebContents* web_contents = MaybeLaunchSystemWebApp(launch_url);
if (web_contents)
return web_contents;
Browser* browser = nullptr;
WindowOpenDisposition navigation_disposition;
std::tie(browser, navigation_disposition) = EnsureBrowser();
web_contents = NavigateBrowser(browser, launch_url, navigation_disposition,
share_target);
if (!web_contents)
return nullptr;
MaybeEnqueueWebLaunchParams(launch_url, is_file_handling, web_contents);
RecordMetrics(launch_url, web_contents);
return web_contents;
}
const apps::ShareTarget* LaunchProcess::MaybeGetShareTarget() {
bool is_share_intent =
params_.intent &&
(params_.intent->action == apps_util::kIntentActionSend ||
params_.intent->action == apps_util::kIntentActionSendMultiple);
return is_share_intent
? provider_.registrar().GetAppShareTarget(params_.app_id)
: nullptr;
}
std::tuple<GURL, bool /*is_file_handling*/> LaunchProcess::GetLaunchUrl(
const apps::ShareTarget* share_target) {
GURL launch_url;
bool is_file_handling = false;
if (!params_.override_url.is_empty()) {
launch_url = params_.override_url;
} else if (params_.url_handler_launch_url.has_value() &&
params_.url_handler_launch_url->is_valid()) {
// Handle url_handlers launch.
launch_url = params_.url_handler_launch_url.value();
} else if (absl::optional<GURL> file_handler_url =
provider_.os_integration_manager().GetMatchingFileHandlerURL(
params_.app_id, params_.launch_files)) {
// Handle file_handlers launch.
launch_url = file_handler_url.value();
is_file_handling = true;
} else if (absl::optional<GURL> protocol_handler_translated_url =
GetProtocolHandlingTranslatedUrl(provider_, params_)) {
// Handle protocol_handlers launch.
launch_url = protocol_handler_translated_url.value();
} else if (share_target) {
// Handle share_target launch.
launch_url = share_target->action;
} else {
// This is a default launch.
launch_url = provider_.registrar().GetAppLaunchUrl(params_.app_id);
}
DCHECK(launch_url.is_valid());
return {launch_url, is_file_handling};
}
WindowOpenDisposition LaunchProcess::GetNavigationDisposition() {
// Only CURRENT_TAB and NEW_FOREGROUND_TAB dispositions are supported for web
// app launches.
return params_.disposition == WindowOpenDisposition::CURRENT_TAB
? WindowOpenDisposition::CURRENT_TAB
: WindowOpenDisposition::NEW_FOREGROUND_TAB;
}
content::WebContents* LaunchProcess::MaybeLaunchSystemWebApp(
const GURL& launch_url) {
absl::optional<SystemAppType> system_app_type =
GetSystemWebAppTypeForAppId(&profile_, params_.app_id);
if (!system_app_type)
return nullptr;
Browser* browser =
LaunchSystemWebAppImpl(&profile_, *system_app_type, launch_url, params_);
return browser->tab_strip_model()->GetActiveWebContents();
}
std::tuple<Browser*, WindowOpenDisposition> LaunchProcess::EnsureBrowser() {
Browser* browser = MaybeFindBrowserForLaunch();
WindowOpenDisposition navigation_disposition = GetNavigationDisposition();
if (browser) {
browser->window()->Activate();
} else {
browser = CreateBrowserForLaunch();
// By opening a new window we've already performed part of a "disposition",
// the only remaining thing for Navigate() to do is navigate the new window.
navigation_disposition = WindowOpenDisposition::CURRENT_TAB;
// TODO(crbug.com/1200944): Use NEW_FOREGROUND_TAB instead of CURRENT_TAB.
// The window has no tabs so it doesn't make sense to open the "current"
// tab. We use it anyway because it happens to work.
// If NEW_FOREGROUND_TAB is used the the WindowCanOpenTabs() check fails
// when `launch_url` is out of scope for web app windows causing it to
// open another separate browser window. It should be updated to check the
// extended scope.
}
browser->window()->Show();
return {browser, navigation_disposition};
}
Browser* LaunchProcess::MaybeFindBrowserForLaunch() {
if (params_.container == apps::mojom::LaunchContainer::kLaunchContainerTab) {
return chrome::FindTabbedBrowser(
&profile_, /*match_original_profiles=*/false, params_.display_id);
}
if (params_.disposition != WindowOpenDisposition::NEW_FOREGROUND_TAB)
return nullptr;
if (!provider_.registrar().IsTabbedWindowModeEnabled(params_.app_id))
return nullptr;
for (Browser* browser : *BrowserList::GetInstance()) {
if (browser->profile() == &profile_ &&
AppBrowserController::IsForWebApp(browser, params_.app_id)) {
return browser;
}
}
return nullptr;
}
Browser* LaunchProcess::CreateBrowserForLaunch() {
if (params_.container == apps::mojom::LaunchContainer::kLaunchContainerTab) {
return Browser::Create(Browser::CreateParams(Browser::TYPE_NORMAL,
&profile_,
/*user_gesture=*/true));
}
return CreateWebApplicationWindow(&profile_, params_.app_id,
params_.disposition, params_.restore_id);
}
content::WebContents* LaunchProcess::NavigateBrowser(
Browser* browser,
const GURL& launch_url,
WindowOpenDisposition navigation_disposition,
const apps::ShareTarget* share_target) {
if (share_target) {
NavigateParams nav_params =
NavigateParamsForShareTarget(browser, *share_target, *params_.intent);
nav_params.disposition = navigation_disposition;
return NavigateWebAppUsingParams(params_.app_id, nav_params);
}
TabStripModel* const tab_strip = browser->tab_strip_model();
if (tab_strip->empty() ||
navigation_disposition != WindowOpenDisposition::CURRENT_TAB) {
return NavigateWebApplicationWindow(browser, params_.app_id, launch_url,
navigation_disposition);
}
content::WebContents* existing_tab = tab_strip->GetActiveWebContents();
DCHECK(existing_tab);
const int tab_index = tab_strip->GetIndexOfWebContents(existing_tab);
existing_tab->OpenURL(content::OpenURLParams(
launch_url,
content::Referrer::SanitizeForRequest(
launch_url,
content::Referrer(existing_tab->GetURL(),
network::mojom::ReferrerPolicy::kDefault)),
navigation_disposition, ui::PAGE_TRANSITION_AUTO_BOOKMARK,
/*is_renderer_initiated=*/false));
content::WebContents* web_contents = tab_strip->GetActiveWebContents();
tab_strip->ActivateTabAt(tab_index, {TabStripModel::GestureType::kOther});
SetTabHelperAppId(web_contents, params_.app_id);
return web_contents;
}
void LaunchProcess::MaybeEnqueueWebLaunchParams(
const GURL& launch_url,
bool is_file_handling,
content::WebContents* web_contents) {
if (!is_file_handling)
return;
web_launch::WebLaunchFilesHelper::SetLaunchPaths(web_contents, launch_url,
params_.launch_files);
}
void LaunchProcess::RecordMetrics(const GURL& launch_url,
content::WebContents* web_contents) {
// TODO(crbug.com/1014328): Populate WebApp metrics instead of Extensions.
if (params_.container == apps::mojom::LaunchContainer::kLaunchContainerTab) {
UMA_HISTOGRAM_ENUMERATION("Extensions.AppTabLaunchType",
extensions::LAUNCH_TYPE_REGULAR, 100);
} else if (params_.container ==
apps::mojom::LaunchContainer::kLaunchContainerWindow) {
RecordAppWindowLaunch(&profile_, params_.app_id);
}
UMA_HISTOGRAM_ENUMERATION("Extensions.BookmarkAppLaunchSource",
params_.source);
UMA_HISTOGRAM_ENUMERATION("Extensions.BookmarkAppLaunchContainer",
params_.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);
provider_.sync_bridge().SetAppLastLaunchTime(params_.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
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) {
std::string app_name = GenerateApplicationNameFromAppId(app_id);
gfx::Rect initial_bounds;
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;
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);
}
WebAppLaunchManager::WebAppLaunchManager(Profile* profile)
: profile_(profile),
provider_(WebAppProvider::GetForLocalAppsUnchecked(profile)) {}
WebAppLaunchManager::~WebAppLaunchManager() = default;
content::WebContents* WebAppLaunchManager::OpenApplication(
apps::AppLaunchParams&& params) {
if (GetOpenApplicationCallbackForTesting())
return GetOpenApplicationCallbackForTesting().Run(std::move(params));
return LaunchProcess(*profile_, params).Run();
}
void WebAppLaunchManager::LaunchApplication(
const std::string& app_id,
const base::CommandLine& command_line,
const base::FilePath& current_directory,
const absl::optional<GURL>& url_handler_launch_url,
const absl::optional<GURL>& protocol_handler_launch_url,
base::OnceCallback<void(Browser* browser,
apps::mojom::LaunchContainer container)> callback) {
if (!provider_)
return;
apps::mojom::AppLaunchSource launch_source =
apps::mojom::AppLaunchSource::kSourceCommandLine;
if (base::FeatureList::IsEnabled(features::kDesktopPWAsRunOnOsLogin) &&
command_line.HasSwitch(switches::kAppRunOnOsLoginMode)) {
launch_source = apps::mojom::AppLaunchSource::kSourceRunOnOsLogin;
}
apps::AppLaunchParams params(
app_id, apps::mojom::LaunchContainer::kLaunchContainerWindow,
WindowOpenDisposition::NEW_WINDOW, launch_source);
params.command_line = command_line;
params.current_directory = current_directory;
if (!DoesCommandLineContainProtocolUrl(command_line)) {
params.launch_files = apps::GetLaunchFilesFromCommandLine(command_line);
}
params.url_handler_launch_url = url_handler_launch_url;
params.protocol_handler_launch_url = protocol_handler_launch_url;
params.override_url = GURL(command_line.GetSwitchValueASCII(
switches::kAppLaunchUrlForShortcutsMenuItem));
// Wait for the web applications database to load.
// If the profile and WebAppLaunchManager are destroyed,
// on_registry_ready will not fire.
provider_->on_registry_ready().Post(
FROM_HERE, base::BindOnce(&WebAppLaunchManager::LaunchWebApplication,
weak_ptr_factory_.GetWeakPtr(),
std::move(params), std::move(callback)));
}
// static
void WebAppLaunchManager::SetOpenApplicationCallbackForTesting(
OpenApplicationCallback callback) {
GetOpenApplicationCallbackForTesting() = std::move(callback);
}
void WebAppLaunchManager::LaunchWebApplication(
apps::AppLaunchParams&& params,
base::OnceCallback<void(Browser* browser,
apps::mojom::LaunchContainer container)> callback) {
apps::mojom::LaunchContainer container;
Browser* browser = nullptr;
if (provider_->registrar().IsInstalled(params.app_id)) {
if (provider_->registrar().GetAppEffectiveDisplayMode(params.app_id) ==
blink::mojom::DisplayMode::kBrowser) {
params.container = apps::mojom::LaunchContainer::kLaunchContainerTab;
params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
}
container = params.container;
const content::WebContents* web_contents =
OpenApplication(std::move(params));
if (web_contents)
browser = chrome::FindBrowserWithWebContents(web_contents);
} else {
// Open an empty browser window as the app_id is invalid.
container = apps::mojom::LaunchContainer::kLaunchContainerNone;
browser = apps::CreateBrowserWithNewTabPage(profile_);
}
std::move(callback).Run(browser, container);
}
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);
}
} // namespace web_app