blob: f836737afd2d1874765603dbc5d797746b246b19 [file] [log] [blame]
// Copyright 2021 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/web_app_launch_process.h"
#include "base/files/file_path.h"
#include "base/memory/values_equivalent.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.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_navigator_params.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_user_gesture_details.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/web_app_launch_utils.h"
#include "chrome/browser/web_applications/os_integration/os_integration_manager.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_launch_queue.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_app_tab_helper.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/intent.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "extensions/common/constants.h"
#include "third_party/blink/public/common/features.h"
#include "ui/display/scoped_display_for_new_windows.h"
#include "url/gurl.h"
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#endif
namespace web_app {
namespace {
absl::optional<GURL> GetProtocolHandlingTranslatedUrl(
OsIntegrationManager& os_integration_manager,
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 =
os_integration_manager.TranslateProtocolUrl(params.app_id, protocol_url);
return translated_url;
}
} // namespace
// static
content::WebContents* WebAppLaunchProcess::CreateAndRun(
Profile& profile,
WebAppRegistrar& registrar,
OsIntegrationManager& os_integration_manager,
const apps::AppLaunchParams& params) {
return WebAppLaunchProcess(profile, registrar, os_integration_manager, params)
.Run();
}
WebAppLaunchProcess::WebAppLaunchProcess(
Profile& profile,
WebAppRegistrar& registrar,
OsIntegrationManager& os_integration_manager,
const apps::AppLaunchParams& params)
: profile_(profile),
registrar_(registrar),
os_integration_manager_(os_integration_manager),
params_(params),
web_app_(registrar_->GetAppById(params.app_id)) {}
content::WebContents* WebAppLaunchProcess::Run() {
if (Browser::GetCreationStatusForProfile(&profile_.get()) !=
Browser::CreationStatus::kOk ||
!registrar_->IsInstalled(params_->app_id)) {
return nullptr;
}
// Place new windows on the specified display.
absl::optional<display::ScopedDisplayForNewWindows> scoped_display;
if (params_->display_id != display::kInvalidDisplayId) {
scoped_display.emplace(params_->display_id);
}
const apps::ShareTarget* share_target = MaybeGetShareTarget();
auto [launch_url, is_file_handling] = GetLaunchUrl(share_target);
// TODO(crbug.com/1265381): URL Handlers allows web apps to be opened with
// associated origin URLs. There's no utility function to test whether a URL
// is in a web app's extended scope at the moment.
// Because URL Handlers is not implemented for Chrome OS we can perform this
// DCHECK on the basic scope.
#if BUILDFLAG(IS_CHROMEOS_ASH)
bool is_url_in_system_web_app_sccope =
ash::GetSystemWebAppTypeForAppId(&*profile_, params_->app_id) &&
ash::SystemWebAppManager::Get(&*profile_)
->GetSystemApp(
*ash::GetSystemWebAppTypeForAppId(&*profile_, params_->app_id)) &&
ash::SystemWebAppManager::Get(&*profile_)
->GetSystemApp(
*ash::GetSystemWebAppTypeForAppId(&*profile_, params_->app_id))
->IsUrlInSystemAppScope(launch_url);
DCHECK(registrar_->IsUrlInAppScope(launch_url, params_->app_id) ||
is_url_in_system_web_app_sccope);
#else
DCHECK(registrar_->IsUrlInAppScope(launch_url, params_->app_id));
#endif
#if BUILDFLAG(IS_CHROMEOS_ASH)
// System Web Apps have their own launch code path.
absl::optional<ash::SystemWebAppType> system_app_type =
ash::GetSystemWebAppTypeForAppId(&profile_.get(), params_->app_id);
if (system_app_type) {
Browser* browser = LaunchSystemWebAppImpl(&profile_.get(), *system_app_type,
launch_url, *params_);
return browser ? browser->tab_strip_model()->GetActiveWebContents()
: nullptr;
}
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
auto [browser, is_new_browser] = EnsureBrowser();
NavigateResult navigate_result =
MaybeNavigateBrowser(browser, is_new_browser, launch_url, share_target);
content::WebContents* web_contents = navigate_result.web_contents;
if (!web_contents)
return nullptr;
MaybeEnqueueWebLaunchParams(
launch_url, is_file_handling, web_contents,
/*started_new_navigation=*/navigate_result.did_navigate);
UpdateLaunchStats(web_contents, params_->app_id, launch_url);
RecordLaunchMetrics(params_->app_id, params_->container,
apps::GetAppLaunchSource(params_->launch_source),
launch_url, web_contents);
return web_contents;
}
const apps::ShareTarget* WebAppLaunchProcess::MaybeGetShareTarget() const {
DCHECK(web_app_);
bool is_share_intent = params_->intent && params_->intent->IsShareIntent();
return is_share_intent && web_app_->share_target().has_value()
? &web_app_->share_target().value()
: nullptr;
}
std::tuple<GURL, bool /*is_file_handling*/> WebAppLaunchProcess::GetLaunchUrl(
const apps::ShareTarget* share_target) const {
DCHECK(web_app_);
GURL launch_url;
bool is_file_handling = false;
bool is_note_taking_intent =
params_->intent &&
params_->intent->action == apps_util::kIntentActionCreateNote;
if (share_target) {
// Handle share_target launch.
launch_url = share_target->action;
} else if (!params_->override_url.is_empty()) {
launch_url = params_->override_url;
is_file_handling = !params_->launch_files.empty();
} 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> protocol_handler_translated_url =
GetProtocolHandlingTranslatedUrl(*os_integration_manager_,
*params_)) {
// Handle protocol_handlers launch.
launch_url = protocol_handler_translated_url.value();
} else if (is_note_taking_intent &&
web_app_->note_taking_new_note_url().is_valid()) {
// Handle Create Note launch.
launch_url = web_app_->note_taking_new_note_url();
} else {
// This is a default launch.
launch_url = registrar_->GetAppLaunchUrl(params_->app_id);
}
DCHECK(launch_url.is_valid());
return {launch_url, is_file_handling};
}
WindowOpenDisposition WebAppLaunchProcess::GetNavigationDisposition(
bool is_new_browser) const {
if (registrar_->IsTabbedWindowModeEnabled(params_->app_id)) {
return WindowOpenDisposition::NEW_FOREGROUND_TAB;
}
if (is_new_browser) {
// 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.
return 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.
}
// If launch handler is routing to an existing client, we want to use the
// existing WebContents rather than opening a new tab.
if (GetLaunchHandler().TargetsExistingClients()) {
return WindowOpenDisposition::CURRENT_TAB;
}
// 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;
}
LaunchHandler WebAppLaunchProcess::GetLaunchHandler() const {
DCHECK(web_app_);
return web_app_->launch_handler().value_or(LaunchHandler());
}
LaunchHandler::ClientMode WebAppLaunchProcess::GetLaunchClientMode() const {
LaunchHandler launch_handler = GetLaunchHandler();
if (launch_handler.client_mode == LaunchHandler::ClientMode::kAuto)
return LaunchHandler::ClientMode::kNavigateNew;
return launch_handler.client_mode;
}
std::tuple<Browser*, bool /*is_new_browser*/>
WebAppLaunchProcess::EnsureBrowser() {
Browser* browser = MaybeFindBrowserForLaunch();
bool is_new_browser = false;
if (browser) {
browser->window()->Activate();
} else {
browser = CreateBrowserForLaunch();
is_new_browser = true;
}
browser->window()->Show();
return {browser, is_new_browser};
}
Browser* WebAppLaunchProcess::MaybeFindBrowserForLaunch() const {
if (params_->container == apps::LaunchContainer::kLaunchContainerTab) {
// In general, when opening a web application in a tab, we want to open the
// application in a tab in the most recently used browser window.
// Chrome OS however prefers opening new tabs in windows on a specific
// display, specifically the one returned by GetDisplayForNewWindows(), even
// if no browser windows are currently open on that display (except when
// we're specifically opening the app in the current tab, rather than a new
// tab).
int64_t display_id = display::kInvalidDisplayId;
#if BUILDFLAG(IS_CHROMEOS)
if (params_->disposition != WindowOpenDisposition::CURRENT_TAB) {
display_id = display::Screen::GetScreen()->GetDisplayForNewWindows().id();
}
#endif
return chrome::FindTabbedBrowser(
&profile_.get(), /*match_original_profiles=*/false, display_id);
}
if (!registrar_->IsTabbedWindowModeEnabled(params_->app_id) &&
GetLaunchClientMode() == LaunchHandler::ClientMode::kNavigateNew) {
return nullptr;
}
return AppBrowserController::FindForWebApp(*profile_, params_->app_id);
}
Browser* WebAppLaunchProcess::CreateBrowserForLaunch() {
if (params_->container == apps::LaunchContainer::kLaunchContainerTab) {
return Browser::Create(Browser::CreateParams(Browser::TYPE_NORMAL,
&profile_.get(),
/*user_gesture=*/true));
}
return CreateWebApplicationWindow(&*profile_, params_->app_id,
params_->disposition, params_->restore_id);
}
WebAppLaunchProcess::NavigateResult WebAppLaunchProcess::MaybeNavigateBrowser(
Browser* browser,
bool is_new_browser,
const GURL& launch_url,
const apps::ShareTarget* share_target) {
WindowOpenDisposition navigation_disposition =
GetNavigationDisposition(is_new_browser);
if (share_target) {
// TODO(crbug.com/1213776): Expose share target in the LaunchParams and
// don't navigate if navigate_existing_client: never is in effect.
NavigateParams nav_params = NavigateParamsForShareTarget(
browser, *share_target, *params_->intent, params_->launch_files);
nav_params.disposition = navigation_disposition;
return {
.web_contents = NavigateWebAppUsingParams(params_->app_id, nav_params),
.did_navigate = true};
}
TabStripModel* const tab_strip = browser->tab_strip_model();
if (tab_strip->empty() ||
navigation_disposition != WindowOpenDisposition::CURRENT_TAB) {
return {.web_contents = NavigateWebApplicationWindow(
browser, params_->app_id, launch_url, navigation_disposition),
.did_navigate = true};
}
content::WebContents* existing_tab = tab_strip->GetActiveWebContents();
DCHECK(existing_tab);
if (GetLaunchHandler().NeverNavigateExistingClients()) {
if (base::ValuesEquivalent(WebAppTabHelper::FromWebContents(existing_tab)
->EnsureLaunchQueue()
.GetPendingLaunchAppId(),
&params_->app_id)) {
// This WebContents is already handling a launch for this app. It may
// currently be out of scope but the in progress app launch will put it
// back in scope. The new app launch params can be queued up to fire after
// the existing app launch completes.
return {.web_contents = existing_tab, .did_navigate = false};
}
if (registrar_->IsUrlInAppScope(existing_tab->GetLastCommittedURL(),
params_->app_id)) {
// If the web contents is currently navigating then interrupt it. The
// current page is now being used for this app launch.
existing_tab->Stop();
return {.web_contents = existing_tab, .did_navigate = false};
}
}
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, TabStripUserGestureDetails(
TabStripUserGestureDetails::GestureType::kOther));
SetWebContentsActingAsApp(web_contents, params_->app_id);
return {.web_contents = web_contents, .did_navigate = true};
}
void WebAppLaunchProcess::MaybeEnqueueWebLaunchParams(
const GURL& launch_url,
bool is_file_handling,
content::WebContents* web_contents,
bool started_new_navigation) {
if (is_file_handling || web_app_->launch_handler().has_value() ||
base::FeatureList::IsEnabled(
blink::features::kWebAppEnableLaunchHandler)) {
WebAppLaunchParams launch_params;
launch_params.started_new_navigation = started_new_navigation;
launch_params.app_id = web_app_->app_id();
launch_params.target_url = launch_url;
launch_params.paths = is_file_handling ? params_->launch_files
: std::vector<base::FilePath>();
WebAppTabHelper::FromWebContents(web_contents)
->EnsureLaunchQueue()
.Enqueue(std::move(launch_params));
}
}
} // namespace web_app