| // Copyright 2021 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_process.h" |
| |
| #include "base/files/file_path.h" |
| #include "base/memory/values_equivalent.h" |
| #include "base/metrics/histogram_macros.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_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_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_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 "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 "components/site_engagement/content/site_engagement_service.h" |
| #include "extensions/common/constants.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 |
| |
| WebAppLaunchProcess::WebAppLaunchProcess(Profile& profile, |
| const apps::AppLaunchParams& params) |
| : profile_(profile), |
| provider_(*WebAppProvider::GetForLocalAppsUnchecked(&profile)), |
| params_(params), |
| web_app_(provider_.registrar().GetAppById(params.app_id)) {} |
| |
| content::WebContents* WebAppLaunchProcess::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(); |
| 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::GetForLocalAppsUnchecked(&profile_) |
| ->GetSystemApp( |
| *ash::GetSystemWebAppTypeForAppId(&profile_, params_.app_id)) && |
| ash::SystemWebAppManager::GetForLocalAppsUnchecked(&profile_) |
| ->GetSystemApp( |
| *ash::GetSystemWebAppTypeForAppId(&profile_, params_.app_id)) |
| ->IsUrlInSystemAppScope(launch_url); |
| DCHECK(provider_.registrar().IsUrlInAppScope(launch_url, params_.app_id) || |
| is_url_in_system_web_app_sccope); |
| #else |
| DCHECK(provider_.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_, params_.app_id); |
| if (system_app_type) { |
| Browser* browser = LaunchSystemWebAppImpl(&profile_, *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); |
| |
| RecordMetrics(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( |
| provider_.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 = provider_.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 (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 (RouteToExistingClient()) { |
| 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::RouteTo WebAppLaunchProcess::GetLaunchRouteTo() const { |
| DCHECK(web_app_); |
| LaunchHandler launch_handler = |
| web_app_->launch_handler().value_or(LaunchHandler()); |
| if (launch_handler.route_to == LaunchHandler::RouteTo::kAuto) |
| return LaunchHandler::RouteTo::kNewClient; |
| return launch_handler.route_to; |
| } |
| |
| bool WebAppLaunchProcess::RouteToExistingClient() const { |
| switch (GetLaunchRouteTo()) { |
| case LaunchHandler::RouteTo::kAuto: |
| case LaunchHandler::RouteTo::kNewClient: |
| return false; |
| case LaunchHandler::RouteTo::kExistingClientNavigate: |
| case LaunchHandler::RouteTo::kExistingClientRetain: |
| return true; |
| } |
| } |
| |
| bool WebAppLaunchProcess::NeverNavigateExistingClients() const { |
| switch (GetLaunchRouteTo()) { |
| case LaunchHandler::RouteTo::kAuto: |
| case LaunchHandler::RouteTo::kNewClient: |
| case LaunchHandler::RouteTo::kExistingClientNavigate: |
| return false; |
| case LaunchHandler::RouteTo::kExistingClientRetain: |
| return true; |
| } |
| } |
| |
| 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) { |
| return chrome::FindTabbedBrowser( |
| &profile_, /*match_original_profiles=*/false, |
| display::Screen::GetScreen()->GetDisplayForNewWindows().id()); |
| } |
| |
| if (!provider_.registrar().IsTabbedWindowModeEnabled(params_.app_id) && |
| GetLaunchRouteTo() == LaunchHandler::RouteTo::kNewClient) { |
| return nullptr; |
| } |
| |
| for (Browser* browser : *BrowserList::GetInstance()) { |
| if (browser->profile() == &profile_ && |
| AppBrowserController::IsForWebApp(browser, params_.app_id)) { |
| return browser; |
| } |
| } |
| |
| return nullptr; |
| } |
| |
| Browser* WebAppLaunchProcess::CreateBrowserForLaunch() { |
| if (params_.container == apps::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); |
| } |
| |
| 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 (NeverNavigateExistingClients()) { |
| if (base::ValuesEquivalent(WebAppTabHelper::FromWebContents(existing_tab) |
| ->EnsureLaunchQueue() |
| .GetPendingLaunchAppId(), |
| ¶ms_.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 (provider_.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()) { |
| 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 |