| // Copyright 2025 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/navigation_capturing_process.h" |
| |
| #include "base/debug/crash_logging.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/string_split.h" |
| #include "base/types/optional_util.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_params.h" |
| #include "chrome/browser/ui/web_applications/app_browser_controller.h" |
| #include "chrome/browser/ui/web_applications/navigation_handle_user_data_forwarder.h" |
| #include "chrome/browser/ui/web_applications/web_app_browser_controller.h" |
| #include "chrome/browser/ui/web_applications/web_app_launch_navigation_handle_user_data.h" |
| #include "chrome/browser/ui/web_applications/web_app_launch_utils.h" |
| #include "chrome/browser/ui/web_applications/web_app_tabbed_utils.h" |
| #include "chrome/browser/web_applications/link_capturing_features.h" |
| #include "chrome/browser/web_applications/navigation_capturing_log.h" |
| #include "chrome/browser/web_applications/navigation_capturing_metrics.h" |
| #include "chrome/browser/web_applications/navigation_capturing_settings.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_provider.h" |
| #include "chrome/browser/web_applications/web_app_registrar.h" |
| #include "chrome/browser/web_applications/web_app_tab_helper.h" |
| #include "chrome/browser/web_applications/web_app_utils.h" |
| #include "content/public/browser/site_isolation_policy.h" |
| #include "content/public/common/content_features.h" |
| #include "third_party/blink/public/mojom/manifest/display_mode.mojom-shared.h" |
| #include "ui/base/page_transition_types.h" |
| #include "ui/base/window_open_disposition.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h" |
| #include "chrome/browser/web_applications/chromeos_web_app_experiments.h" |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| namespace web_app { |
| |
| using BrowserAndTabOverride = NavigationCapturingProcess::BrowserAndTabOverride; |
| |
| namespace { |
| |
| bool IsDispositionValidForNavigationCapturing( |
| WindowOpenDisposition disposition) { |
| switch (disposition) { |
| case WindowOpenDisposition::NEW_FOREGROUND_TAB: |
| case WindowOpenDisposition::NEW_BACKGROUND_TAB: |
| case WindowOpenDisposition::NEW_WINDOW: |
| return true; |
| case WindowOpenDisposition::UNKNOWN: |
| // Note: App popups are handled in browser_navigator.cc |
| case WindowOpenDisposition::NEW_POPUP: |
| case WindowOpenDisposition::CURRENT_TAB: |
| case WindowOpenDisposition::SINGLETON_TAB: |
| case WindowOpenDisposition::SAVE_TO_DISK: |
| case WindowOpenDisposition::OFF_THE_RECORD: |
| case WindowOpenDisposition::IGNORE_ACTION: |
| case WindowOpenDisposition::SWITCH_TO_TAB: |
| case WindowOpenDisposition::NEW_PICTURE_IN_PICTURE: |
| return false; |
| } |
| } |
| |
| bool IsPageTransitionValidForNavigationCapturing( |
| ui::PageTransition transition) { |
| switch (ui::PageTransitionStripQualifier(transition)) { |
| case ui::PAGE_TRANSITION_TYPED: |
| case ui::PAGE_TRANSITION_AUTO_TOPLEVEL: |
| case ui::PAGE_TRANSITION_AUTO_BOOKMARK: |
| case ui::PAGE_TRANSITION_AUTO_SUBFRAME: |
| case ui::PAGE_TRANSITION_MANUAL_SUBFRAME: |
| case ui::PAGE_TRANSITION_GENERATED: |
| case ui::PAGE_TRANSITION_RELOAD: |
| case ui::PAGE_TRANSITION_KEYWORD: |
| case ui::PAGE_TRANSITION_KEYWORD_GENERATED: |
| return false; |
| case ui::PAGE_TRANSITION_LINK: |
| case ui::PAGE_TRANSITION_FORM_SUBMIT: |
| break; |
| default: |
| NOTREACHED(); |
| } |
| if (base::to_underlying(ui::PageTransitionGetQualifier(transition)) != 0) { |
| // Qualifiers indicate that this navigation was the result of a click on a |
| // forward/back button, typing in the URL bar, or client-side redirections. |
| // Don't handle any of those types of navigations. |
| return false; |
| } |
| return true; |
| } |
| |
| // Returns true if an auxiliary browsing context is getting created, so |
| // navigation should be done in the same container that it was triggered in. |
| bool IsAuxiliaryBrowsingContext(const NavigateParams& nav_params) { |
| if ((nav_params.contents_to_insert && |
| nav_params.contents_to_insert->HasOpener()) || |
| nav_params.opener) { |
| return true; |
| } |
| return false; |
| } |
| |
| Browser* CreateWebAppWindowFromNavigationParams( |
| const webapps::AppId& app_id, |
| const NavigateParams& navigate_params, |
| std::optional<bool> trusted_source_override = std::nullopt) { |
| Browser::CreateParams app_browser_params = CreateParamsForApp( |
| app_id, /*is_popup=*/false, |
| /*trusted_source=*/ |
| trusted_source_override.value_or(navigate_params.trusted_source), |
| navigate_params.window_features.bounds, |
| navigate_params.initiating_profile, navigate_params.user_gesture); |
| Browser* created_browser = |
| CreateWebAppWindowMaybeWithHomeTab(app_id, app_browser_params); |
| return created_browser; |
| } |
| |
| // TODO(crbug.com/371237535): Move to TabInterface once there is support for |
| // getting the browser interface for web contents that are in an app window. |
| // For all use-cases where a reparenting to an app window happens, launch params |
| // need to be enqueued so as to mimic the pre redirection behavior. See |
| // https://bit.ly/pwa-navigation-capturing?tab=t.0#bookmark=id.60x2trlfg6iq for |
| // more information. |
| void ReparentToAppBrowser(content::WebContents* old_web_contents, |
| const webapps::AppId& app_id, |
| blink::mojom::DisplayMode target_display_mode, |
| const GURL& target_url) { |
| Browser* main_browser = chrome::FindBrowserWithTab(old_web_contents); |
| Browser* target_browser = nullptr; |
| if (target_display_mode == blink::mojom::DisplayMode::kTabbed) { |
| target_browser = |
| AppBrowserController::FindForWebApp(*main_browser->profile(), app_id); |
| // If somehow we found a browser that doesn't have a tab strip (which |
| // might be possible if the manifest updated while a window is open), |
| // don't return it to use for new tabs. |
| if (target_browser && !target_browser->app_controller()->has_tab_strip()) { |
| target_browser = nullptr; |
| } |
| } |
| if (!target_browser) { |
| target_browser = CreateWebAppWindowMaybeWithHomeTab( |
| app_id, |
| CreateParamsForApp(app_id, /*is_popup=*/false, /*trusted_source=*/true, |
| gfx::Rect(), main_browser->profile(), |
| /*user_gesture=*/true)); |
| } |
| CHECK(target_browser->app_controller()); |
| ReparentWebContentsIntoBrowserImpl( |
| main_browser, old_web_contents, target_browser, |
| target_browser->app_controller()->IsUrlInHomeTabScope(target_url)); |
| CHECK(old_web_contents); |
| } |
| |
| // TODO(crbug.com/371237535): Move to TabInterface once there is support for |
| // getting the browser interface for web contents that are in an app window. |
| void ReparentWebContentsToTabbedBrowser(content::WebContents* old_web_contents, |
| WindowOpenDisposition disposition, |
| Browser* navigate_params_browser) { |
| Browser* source_browser = chrome::FindBrowserWithTab(old_web_contents); |
| Browser* existing_browser_window = |
| navigate_params_browser && |
| !AppBrowserController::IsWebApp(navigate_params_browser) |
| ? navigate_params_browser |
| : chrome::FindTabbedBrowser(source_browser->profile(), |
| /*match_original_profiles=*/false); |
| |
| // Create a new browser window if the navigation was triggered via a |
| // shift-click, or if there are no open tabbed browser windows at the moment. |
| Browser* target_browser_window = |
| (disposition == WindowOpenDisposition::NEW_WINDOW || |
| !existing_browser_window) |
| ? Browser::Create(Browser::CreateParams(source_browser->profile(), |
| /*user_gesture=*/true)) |
| : existing_browser_window; |
| |
| ReparentWebContentsIntoBrowserImpl(source_browser, old_web_contents, |
| target_browser_window); |
| } |
| |
| Browser* FindNormalBrowser(const Profile& profile) { |
| for (Browser* browser : BrowserList::GetInstance()->OrderedByActivation()) { |
| if (browser->is_type_normal() && browser->profile() == &profile) { |
| return browser; |
| } |
| } |
| return nullptr; |
| } |
| |
| // Record the result of navigation capturing before redirection happens or a |
| // network request has been made. |
| void RecordInitialNavigationCapturingResult( |
| NavigationCapturingInitialResult result) { |
| base::UmaHistogramEnumeration("Webapp.NavigationCapturing.Result", result); |
| } |
| |
| } // namespace |
| |
| // static |
| std::unique_ptr<NavigationCapturingProcess> |
| NavigationCapturingProcess::MaybeHandleAppNavigation( |
| const NavigateParams& params) { |
| Profile* profile = params.initiating_profile; |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // System Web Apps should not be going through the navigation capturing |
| // process. |
| const std::optional<ash::SystemWebAppType> capturing_system_app_type = |
| ash::GetCapturingSystemAppForURL(profile, params.url); |
| if (capturing_system_app_type.has_value()) { |
| if (params.browser && |
| ash::IsBrowserForSystemWebApp(params.browser, |
| capturing_system_app_type.value())) { |
| RecordInitialNavigationCapturingResult( |
| NavigationCapturingInitialResult::kNotHandled); |
| return nullptr; |
| } |
| // This process should never be called for URLS captured by system web apps |
| // from a non-system-web-app browser. |
| NOTREACHED(); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| if (!AreWebAppsUserInstallable(profile) || |
| Browser::GetCreationStatusForProfile(profile) != |
| Browser::CreationStatus::kOk || |
| !params.url.is_valid()) { |
| RecordInitialNavigationCapturingResult( |
| NavigationCapturingInitialResult::kNotHandled); |
| return nullptr; |
| } |
| |
| web_app::WebAppProvider* provider = |
| web_app::WebAppProvider::GetForWebApps(profile); |
| if (!provider) { |
| RecordInitialNavigationCapturingResult( |
| NavigationCapturingInitialResult::kNotHandled); |
| return nullptr; |
| } |
| |
| auto result = base::WrapUnique(new NavigationCapturingProcess(params)); |
| const bool should_handle_navigations_in_app = |
| result->IsNavigationCapturingReimplExperimentEnabled() || |
| params.is_service_worker_open_window || |
| content::SiteIsolationPolicy::ShouldUrlUseApplicationIsolationLevel( |
| profile, params.url); |
| |
| if (!should_handle_navigations_in_app) { |
| // Don't record debug information for ALL navigations unless expensive |
| // DCHECKs are enabled. |
| #if !EXPENSIVE_DCHECKS_ARE_ON() |
| result->debug_data_.clear(); |
| #endif |
| return nullptr; |
| } |
| return result; |
| } |
| |
| NavigationCapturingProcess::NavigationCapturingProcess( |
| const NavigateParams& params) |
| : profile_(raw_ref<Profile>::from_ptr(params.initiating_profile)), |
| source_browser_app_id_( |
| params.browser && |
| web_app::AppBrowserController::IsWebApp(params.browser) |
| ? std::optional(params.browser->app_controller()->app_id()) |
| : std::nullopt), |
| source_tab_app_id_(params.source_contents |
| ? base::OptionalFromPtr(WebAppTabHelper::GetAppId( |
| params.source_contents)) |
| : std::nullopt), |
| navigation_params_url_(params.url), |
| disposition_(params.disposition), |
| navigation_params_browser_(params.browser.get()) { |
| CHECK(AreWebAppsUserInstallable(&*profile_)); |
| CHECK(params.url.is_valid()); |
| |
| WebAppProvider* provider = WebAppProvider::GetForWebApps(&*profile_); |
| CHECK(provider); |
| web_app::WebAppRegistrar& registrar = provider->registrar_unsafe(); |
| navigation_capturing_settings_ = |
| NavigationCapturingSettings::Create(*profile_); |
| |
| debug_data_.Set("referrer.url", params.referrer.url.possibly_invalid_spec()); |
| debug_data_.Set("source_contents_url", |
| params.source_contents |
| ? params.source_contents->GetLastCommittedURL() |
| .possibly_invalid_spec() |
| : "<nullptr>"); |
| debug_data_.Set("navigation_params_opener", params.opener != nullptr); |
| debug_data_.Set("navigation_params_contents_to_insert", |
| base::ToString(params.contents_to_insert.get())); |
| debug_data_.Set("params.transition", |
| ui::PageTransitionGetCoreTransitionString( |
| ui::PageTransitionStripQualifier(params.transition))); |
| debug_data_.Set( |
| "params.transition.qualifiers", |
| static_cast<int>(ui::PageTransitionGetQualifier(params.transition))); |
| |
| first_navigation_app_id_ = |
| navigation_capturing_settings_->GetCapturingWebAppForUrl(params.url); |
| |
| if (first_navigation_app_id_) { |
| CHECK(registrar.GetAppById(*first_navigation_app_id_)); |
| first_navigation_app_display_mode_ = |
| registrar.GetAppEffectiveDisplayMode(*first_navigation_app_id_); |
| } |
| |
| isolated_web_app_navigation_ = |
| content::SiteIsolationPolicy::ShouldUrlUseApplicationIsolationLevel( |
| &profile_.get(), params.url); |
| } |
| |
| NavigationCapturingProcess::~NavigationCapturingProcess() { |
| bool record = navigation_capturing_enabled_; |
| #if EXPENSIVE_DCHECKS_ARE_ON() |
| record = true; |
| #endif |
| |
| RecordInitialNavigationCapturingResult(initial_nav_handling_result_); |
| |
| if (redirection_result_.has_value()) { |
| base::UmaHistogramEnumeration( |
| "Webapp.NavigationCapturing.Redirection.FinalResult", |
| redirection_result_.value()); |
| } |
| |
| if (!debug_data_.empty() && record) { |
| WebAppProvider* provider = WebAppProvider::GetForWebApps(&*profile_); |
| provider->navigation_capturing_log().LogData( |
| "NavigationCapturingProcess", |
| base::Value(std::move(PopulateAndGetDebugData())), |
| navigation_handle_id_); |
| } |
| } |
| |
| // static |
| void NavigationCapturingProcess::AttachToNavigationHandle( |
| content::NavigationHandle& navigation_handle, |
| std::unique_ptr<NavigationCapturingProcess> user_data) { |
| if (!user_data->IsHandledByNavigationCapturing()) { |
| return; |
| } |
| CHECK(!user_data->navigation_handle_); |
| user_data->navigation_handle_ = &navigation_handle; |
| CHECK(user_data->state_ == PipelineState::kInitialOverrideCalculated || |
| user_data->state_ == PipelineState::kAttachedToWebContents); |
| user_data->state_ = PipelineState::kAttachedToNavigationHandle; |
| user_data->OnAttachedToNavigationHandle(); |
| navigation_handle.SetUserData(UserDataKey(), std::move(user_data)); |
| } |
| |
| // static |
| void NavigationCapturingProcess::AttachToNextNavigationInWebContents( |
| content::WebContents& web_contents, |
| std::unique_ptr<NavigationCapturingProcess> user_data) { |
| if (!user_data->IsHandledByNavigationCapturing()) { |
| return; |
| } |
| CHECK_EQ(user_data->state_, PipelineState::kInitialOverrideCalculated); |
| user_data->state_ = PipelineState::kAttachedToWebContents; |
| // If there already is a user data with the same user data key attached to the |
| // web contents, we want to overwrite that user data to make sure the newest |
| // process gets attached to the next navigation. As such we don't check for |
| // existing user data. |
| GURL target_url = user_data->navigation_params_url_; |
| web_contents.SetUserData( |
| UserDataKey(), |
| std::make_unique< |
| NavigationHandleUserDataForwarder<NavigationCapturingProcess>>( |
| web_contents, std::move(user_data), target_url)); |
| } |
| |
| BrowserAndTabOverride |
| NavigationCapturingProcess::GetInitialBrowserAndTabOverrideForNavigation( |
| const NavigateParams& params) { |
| CHECK(AreWebAppsUserInstallable(&*profile_)); |
| CHECK(params.url.is_valid()); |
| CHECK_EQ(state_, PipelineState::kCreated); |
| |
| WebAppProvider* provider = WebAppProvider::GetForWebApps(&*profile_); |
| CHECK(provider); |
| web_app::WebAppRegistrar& registrar = provider->registrar_unsafe(); |
| |
| // Only proceed as below if the navigation capturing is enabled. The flag in |
| // the redirection info has to store the result of this check, so that the |
| // logic in `OnWebAppNavigationAfterWebContentsCreation()` is skipped when not |
| // needed. |
| navigation_capturing_enabled_ = |
| IsNavigationCapturingReimplExperimentEnabled(); |
| debug_data_.Set("is_service_worker_clients_open_window", |
| params.is_service_worker_open_window); |
| |
| if (isolated_web_app_navigation_) { |
| return HandleIsolatedWebAppNavigation(params); |
| } |
| |
| CHECK(!isolated_web_app_navigation_); |
| // Handle service worker related navigations if any here. |
| if (params.is_service_worker_open_window && !navigation_capturing_enabled_) { |
| // See service_worker_client_utils::OpenWindow() for more details. |
| CHECK(!params.browser); |
| CHECK_EQ(disposition_, WindowOpenDisposition::NEW_FOREGROUND_TAB); |
| CHECK(ui::PageTransitionCoreTypeIs(params.transition, |
| ui::PAGE_TRANSITION_AUTO_TOPLEVEL)); |
| |
| if (std::optional<webapps::AppId> app_id = |
| web_app::FindInstalledAppWithUrlInScope(&profile_.get(), params.url, |
| /*window_only=*/true)) { |
| Browser* host_window = CreateWebAppWindowFromNavigationParams( |
| *app_id, params, /*trusted_source_override=*/true); |
| return NoCapturingOverrideBrowser(host_window); |
| } |
| |
| return CapturingDisabled(); |
| } |
| |
| // Below here handles the states outlined in |
| // https://bit.ly/pwa-navigation-capturing |
| if (!navigation_capturing_enabled_) { |
| return CapturingDisabled(); |
| } |
| if (params.started_from_context_menu || |
| params.pwa_navigation_capturing_force_off || |
| params.tabstrip_index != -1) { |
| return CapturingDisabled(); |
| } |
| if (!IsDispositionValidForNavigationCapturing(disposition_)) { |
| return CapturingDisabled(); |
| } |
| |
| // The service worker clients API currently uses |
| // PAGE_TRANSITION_AUTO_TOPLEVEL, which is normally considered invalid for |
| // navigation capturing. Explicitly allow that. |
| if (!params.is_service_worker_open_window && |
| !IsPageTransitionValidForNavigationCapturing(params.transition)) { |
| return CapturingDisabled(); |
| } |
| bool is_for_new_browser = |
| params.browser && params.browser->tab_strip_model()->count() == 0 && |
| (disposition_ == WindowOpenDisposition::NEW_FOREGROUND_TAB || |
| disposition_ == WindowOpenDisposition::NEW_BACKGROUND_TAB); |
| if (is_for_new_browser) { |
| // Some calls to `Navigate` populate a newly created browser in |
| // `params.browser`, with no tabs. Callers can assume that browser is used, |
| // so enabling capturing cause the user to see a browser with no tabs. We |
| // cannot simply close it, as sometimes callers then use/reference that |
| // browser. While that is likely a bug (callers should use the |
| // `params.browser` to be compatible with other logic that changes the |
| // browser), disable capturing in this case for now. |
| return CapturingDisabled(); |
| } |
| |
| // Ensure that we proceed for a `first_navigation_app_display_mode_` to be |
| // navigation captured only for the use-cases that are supported. |
| if (first_navigation_app_display_mode_) { |
| CHECK(WebAppRegistrar::IsSupportedDisplayModeForNavigationCapture( |
| *first_navigation_app_display_mode_)); |
| } |
| |
| std::optional<ClientModeAndBrowser> client_mode_and_browser; |
| if (first_navigation_app_id_) { |
| client_mode_and_browser = |
| GetEffectiveClientModeAndBrowser(*first_navigation_app_id_, params.url); |
| debug_data_.Set( |
| "first_navigation_app_effective_client_mode", |
| base::ToString(client_mode_and_browser->effective_client_mode)); |
| CHECK(client_mode_and_browser->effective_client_mode != |
| LaunchHandler::ClientMode::kAuto); |
| debug_data_.Set("first_navigation_app_host_browser", |
| client_mode_and_browser->browser |
| ? base::ToString(client_mode_and_browser->browser) |
| : "<none>"); |
| debug_data_.Set("first_navigation_app_host_tab", |
| client_mode_and_browser->tab_index.has_value() |
| ? base::ToString(*client_mode_and_browser->tab_index) |
| : "<none>"); |
| } |
| |
| // Case: Any click (user modified or non-modified) with auxiliary browsing |
| // context. Only needs to be handled if it is triggered in the context of an |
| // app browser. |
| if (IsAuxiliaryBrowsingContext(params)) { |
| debug_data_.Set("is_auxiliary_browsing_context", true); |
| if (!navigation_capturing_settings_ |
| ->ShouldAuxiliaryContextsKeepSameContainer(source_browser_app_id_, |
| params.url)) { |
| return CapturingDisabled(); |
| } |
| if (source_browser_app_id_.has_value()) { |
| Browser* app_window = CreateWebAppWindowFromNavigationParams( |
| *source_browser_app_id_, params); |
| |
| return AuxiliaryContextInAppWindow(app_window); |
| } |
| return AuxiliaryContext(); |
| } |
| debug_data_.Set("is_auxiliary_browsing_context", false); |
| |
| // If no app controls the the target url, then there cannot be any further |
| // capturing logic unless a redirect occurs. |
| if (!first_navigation_app_id_) { |
| return NoInitialActionRedirectionHandlingEligible(); |
| } |
| |
| CHECK(first_navigation_app_display_mode_); |
| CHECK(client_mode_and_browser); |
| const webapps::AppId& app_id = *first_navigation_app_id_; |
| blink::mojom::DisplayMode app_display_mode = |
| *first_navigation_app_display_mode_; |
| LaunchHandler::ClientMode client_mode = |
| client_mode_and_browser->effective_client_mode; |
| |
| // Case: User-modified clicks. |
| if (is_user_modified_click()) { |
| // The default behavior is only modified if the source is an app browser or |
| // the controlling app's display mode is 'browser'. |
| if (!source_browser_app_id_.has_value() && |
| app_display_mode != DisplayMode::kBrowser) { |
| return NoInitialActionRedirectionHandlingEligible(); |
| } |
| // Case: Shift-clicks with a new top level browsing context. |
| if (disposition_ == WindowOpenDisposition::NEW_WINDOW) { |
| Browser* app_host_window; |
| if (app_display_mode == DisplayMode::kBrowser) { |
| app_host_window = Browser::Create( |
| Browser::CreateParams(&*profile_, params.user_gesture)); |
| } else { |
| app_host_window = |
| CreateWebAppWindowFromNavigationParams(app_id, params); |
| } |
| return ForcedNewAppContext(app_display_mode, app_host_window); |
| } |
| |
| bool is_in_source_app_with_url_in_scope = |
| source_browser_app_id_ |
| ? registrar.IsUrlInAppScope(params.url, *source_browser_app_id_) |
| : false; |
| |
| // Case: Middle clicks with a new top level browsing context. |
| if (disposition_ == WindowOpenDisposition::NEW_BACKGROUND_TAB && |
| (app_display_mode == DisplayMode::kBrowser || |
| (app_id == source_browser_app_id_ && |
| is_in_source_app_with_url_in_scope))) { |
| if (source_browser_app_id_.has_value() && |
| !params.browser->app_controller()->ShouldHideNewTabButton()) { |
| // Apps that support tabbed mode can open a new tab in the current app |
| // browser itself. |
| return ForcedNewAppContext(app_display_mode, params.browser); |
| } |
| Browser* app_host_window; |
| if (app_display_mode == DisplayMode::kBrowser) { |
| // For a 'new tab' with the 'browser' requested display mode, prefer |
| // using an existing browser window. |
| app_host_window = client_mode_and_browser->browser |
| ? client_mode_and_browser->browser.get() |
| : Browser::Create(Browser::CreateParams( |
| &*profile_, params.user_gesture)); |
| } else { |
| app_host_window = |
| CreateWebAppWindowFromNavigationParams(app_id, params); |
| } |
| return ForcedNewAppContext(app_display_mode, app_host_window); |
| } |
| return NoInitialActionRedirectionHandlingEligible(); |
| } |
| |
| if (disposition_ != WindowOpenDisposition::NEW_FOREGROUND_TAB) { |
| return CapturingDisabled(); |
| } |
| |
| // Case: Left click, non-user-modified. Capturable. |
| |
| // Opening in non-browser-tab requires OS integration. Since os integration |
| // cannot be triggered synchronously, treat this as opening in browser. |
| if (registrar.GetInstallState(app_id) == |
| proto::INSTALLED_WITHOUT_OS_INTEGRATION) { |
| app_display_mode = blink::mojom::DisplayMode::kBrowser; |
| } |
| |
| // Prevent-close requires only focusing the existing tab, and never |
| // navigating. |
| if (registrar.IsPreventCloseEnabled(app_id) && |
| !registrar.IsTabbedWindowModeEnabled(app_id)) { |
| client_mode = LaunchHandler::ClientMode::kFocusExisting; |
| } |
| debug_data_.Set("client_mode", base::ToString(client_mode)); |
| |
| // Focus existing. |
| if (client_mode == LaunchHandler::ClientMode::kFocusExisting) { |
| CHECK(client_mode_and_browser->browser); |
| CHECK(client_mode_and_browser->tab_index.has_value()); |
| return CapturedFocusExisting(client_mode_and_browser->browser, |
| *client_mode_and_browser->tab_index, |
| params.url); |
| } |
| |
| // Navigate existing. |
| if (client_mode == LaunchHandler::ClientMode::kNavigateExisting) { |
| CHECK(client_mode_and_browser->browser); |
| CHECK(client_mode_and_browser->tab_index.has_value()); |
| return CapturedNavigateExisting(client_mode_and_browser->browser, |
| *client_mode_and_browser->tab_index); |
| } |
| |
| // Navigate new. |
| CHECK(client_mode == LaunchHandler::ClientMode::kNavigateNew); |
| |
| Browser* host_window = nullptr; |
| switch (app_display_mode) { |
| case blink::mojom::DisplayMode::kBrowser: |
| if (client_mode_and_browser->browser) { |
| host_window = client_mode_and_browser->browser; |
| } else { |
| host_window = Browser::Create( |
| Browser::CreateParams(&*profile_, params.user_gesture)); |
| } |
| break; |
| case blink::mojom::DisplayMode::kMinimalUi: |
| case blink::mojom::DisplayMode::kStandalone: |
| case blink::mojom::DisplayMode::kWindowControlsOverlay: |
| case blink::mojom::DisplayMode::kBorderless: |
| host_window = CreateWebAppWindowFromNavigationParams(app_id, params); |
| break; |
| case blink::mojom::DisplayMode::kTabbed: |
| if (client_mode_and_browser->browser) { |
| host_window = client_mode_and_browser->browser; |
| } else { |
| host_window = CreateWebAppWindowFromNavigationParams(app_id, params); |
| } |
| CHECK(host_window->app_controller()->has_tab_strip()); |
| if (host_window->app_controller()->IsUrlInHomeTabScope(params.url)) { |
| return CapturedNavigateExisting(host_window, 0); |
| } |
| break; |
| case blink::mojom::DisplayMode::kUndefined: |
| case blink::mojom::DisplayMode::kPictureInPicture: |
| case blink::mojom::DisplayMode::kFullscreen: |
| NOTREACHED(); |
| } |
| |
| return CapturedNewClient(app_display_mode, host_window); |
| } |
| |
| BrowserAndTabOverride |
| NavigationCapturingProcess::HandleIsolatedWebAppNavigation( |
| const NavigateParams& params) { |
| CHECK(isolated_web_app_navigation_); |
| |
| if (!first_navigation_app_id_) { |
| return CancelInitialNavigation( |
| NavigationCapturingInitialResult::kNavigationCanceled); |
| } |
| // App popups and picture-in-picture are handled in the switch statement in |
| // `GetBrowserAndTabForDisposition()`. |
| if (disposition_ == WindowOpenDisposition::NEW_POPUP || |
| disposition_ == WindowOpenDisposition::NEW_PICTURE_IN_PICTURE) { |
| return CapturingDisabled(); |
| } |
| |
| const webapps::AppId& iwa_id = *first_navigation_app_id_; |
| |
| // Prefer `params.browser` if it's a compatible IWA browser. |
| bool iwa_browser = |
| params.browser && |
| web_app::AppBrowserController::IsForWebApp(params.browser, iwa_id); |
| if (iwa_browser) { |
| if (disposition_ == WindowOpenDisposition::CURRENT_TAB) { |
| return CapturingDisabled(); |
| } |
| |
| // If the browser window does not yet have any tabs, and we are |
| // attempting to add the first tab to it, allow for it to be reused. |
| bool navigating_new_tab = |
| disposition_ == WindowOpenDisposition::NEW_FOREGROUND_TAB || |
| disposition_ == WindowOpenDisposition::NEW_BACKGROUND_TAB; |
| if (navigating_new_tab && params.browser->tab_strip_model()->empty()) { |
| return CapturingDisabled(); |
| } |
| } |
| |
| Browser* host_window = CreateWebAppWindowFromNavigationParams( |
| iwa_id, params, /*trusted_source_override=*/true); |
| return NoCapturingOverrideBrowser(host_window); |
| } |
| |
| // static |
| void NavigationCapturingProcess::AfterWebContentsCreation( |
| std::unique_ptr<NavigationCapturingProcess> process, |
| content::WebContents& web_contents, |
| content::NavigationHandle* navigation_handle) { |
| if (navigation_handle) { |
| AttachToNavigationHandle(*navigation_handle, std::move(process)); |
| } else { |
| AttachToNextNavigationInWebContents(web_contents, std::move(process)); |
| } |
| } |
| |
| NavigationCapturingProcess::ThrottleCheckResult |
| NavigationCapturingProcess::HandleRedirect() { |
| CHECK(navigation_handle()); |
| CHECK_EQ(state_, PipelineState::kAttachedToNavigationHandle); |
| navigation_handle_id_ = navigation_handle()->GetNavigationId(); |
| state_ = PipelineState::kFinished; |
| |
| // See https://bit.ly/pwa-navigation-capturing and |
| // https://bit.ly/pwa-navigation-handling-dd for more context. |
| // Exit early if: |
| // 1. If there were no redirects, then the only url in the redirect chain |
| // should be the last url to go to. |
| // 2. This is not a server side redirect. |
| // 3. The navigation was started from the context menu. |
| if (navigation_handle()->GetRedirectChain().size() == 1 || |
| !navigation_handle()->WasServerRedirect() || |
| (navigation_handle()->WasStartedFromContextMenu())) { |
| debug_data_.Set("!redirection_result", "ineligible"); |
| redirection_result_ = NavigationCapturingRedirectionResult::kNotHandled; |
| return content::NavigationThrottle::PROCEED; |
| } |
| const GURL& final_url = navigation_handle()->GetURL(); |
| debug_data_.Set("!redirection_final_url", final_url.possibly_invalid_spec()); |
| if (!final_url.is_valid()) { |
| redirection_result_ = NavigationCapturingRedirectionResult::kNotHandled; |
| return content::NavigationThrottle::PROCEED; |
| } |
| |
| // Do not handle redirections for navigations that create an auxiliary |
| // browsing context, or if the app window that opened is not a part of the |
| // navigation handling flow. |
| if (initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kNotHandled || |
| initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kAuxiliaryContextAppBrowserTab || |
| initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kAuxiliaryContextAppWindow) { |
| redirection_result_ = NavigationCapturingRedirectionResult::kNotHandled; |
| return content::NavigationThrottle::PROCEED; |
| } |
| |
| content::WebContents* const web_contents_for_navigation = |
| navigation_handle()->GetWebContents(); |
| |
| WebAppProvider* provider = |
| WebAppProvider::GetForWebContents(web_contents_for_navigation); |
| WebAppRegistrar& registrar = provider->registrar_unsafe(); |
| std::optional<webapps::AppId> target_app_id = |
| navigation_capturing_settings_->GetCapturingWebAppForUrl(final_url); |
| |
| // "Same first navigation state" case: |
| // First, we can exit early if the first navigation app id matches the target |
| // app id (which includes if they are both std::nullopt), as this means we |
| // already did the 'correct' navigation capturing behavior on the first |
| // navigation. |
| if (first_navigation_app_id_ == target_app_id) { |
| debug_data_.Set("!redirection_result", "Same app."); |
| redirection_result_ = NavigationCapturingRedirectionResult::kSameContext; |
| return content::NavigationThrottle::PROCEED; |
| } |
| |
| // Clear out the "launch app id" field. This way we ensure that in any branch |
| // where the redirect does not result in an app being launched we don't |
| // accidentally (try to) treat it as a launch. Any branch where an app launch |
| // does happen will re-set the field to the correct value. |
| SetLaunchedAppId(std::nullopt); |
| |
| // After this point: |
| // - The browsing context is a top-level browsing context. |
| // - The initial navigation capturing app_id does not match the final |
| // target_app_id (and either can be std::nullopt, but not both). |
| // - Navigation is only triggered as part of left, middle or shift clicks. |
| |
| bool is_source_app_matching_final_target = |
| target_app_id == source_browser_app_id_; |
| |
| // First, handle cases where the final url is not in scope of any app. These |
| // can mostly proceed as is, except for two cases where the initial navigation |
| // ended up in an app window but should now be in a browser tab. |
| if (!target_app_id.has_value()) { |
| if (initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kForcedContextAppWindow || |
| initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kNewAppWindow) { |
| debug_data_.Set("!redirection_result", "Reparent, btab"); |
| ReparentWebContentsToTabbedBrowser(web_contents_for_navigation, |
| disposition_, |
| navigation_params_browser_); |
| redirection_result_ = |
| NavigationCapturingRedirectionResult::kReparentBrowserTabToBrowserTab; |
| } else { |
| debug_data_.Set("!redirection_result", "Noop1"); |
| redirection_result_ = NavigationCapturingRedirectionResult::kSameContext; |
| } |
| return content::NavigationThrottle::PROCEED; |
| } |
| |
| CHECK(registrar.GetAppById(*target_app_id)); |
| blink::mojom::DisplayMode target_display_mode = |
| registrar.GetAppEffectiveDisplayMode(*target_app_id); |
| CHECK(WebAppRegistrar::IsSupportedDisplayModeForNavigationCapture( |
| target_display_mode)); |
| |
| // For the remaining cases we know that the navigation ended up in scope of a |
| // target application. |
| |
| // First, handle the case where a new app container (app or browser) was |
| // force-created for an app for user modified clicks. This refers to the |
| // use-cases here: |
| // https://bit.ly/pwa-navigation-capturing?tab=t.0#bookmark=id.ugh0e993wsl8, |
| // where a new app container is made. |
| if (initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kForcedContextAppWindow) { |
| CHECK(source_browser_app_id_.has_value()); |
| CHECK(first_navigation_app_id_.has_value()); |
| // standalone-app -> browser-tab-app. |
| if (target_display_mode == blink::mojom::DisplayMode::kBrowser) { |
| debug_data_.Set("!redirection_result", "app to btab"); |
| SetLaunchedAppId(*target_app_id, /*force_iph_off=*/true); |
| ReparentWebContentsToTabbedBrowser(web_contents_for_navigation, |
| disposition_, |
| navigation_params_browser_); |
| redirection_result_ = |
| NavigationCapturingRedirectionResult::kReparentAppToBrowserTab; |
| return content::NavigationThrottle::PROCEED; |
| } |
| debug_data_.Set("!redirection_result", "app to app"); |
| // standalone-app -> standalone-app. |
| SetLaunchedAppId(*target_app_id); |
| CHECK(target_display_mode != blink::mojom::DisplayMode::kBrowser); |
| ReparentToAppBrowser(web_contents_for_navigation, *target_app_id, |
| target_display_mode, final_url); |
| redirection_result_ = |
| NavigationCapturingRedirectionResult::kReparentAppToApp; |
| return content::NavigationThrottle::PROCEED; |
| } |
| if (initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kForcedContextAppBrowserTab) { |
| // browser-tab-app -> browser-tab-app. |
| if (target_display_mode == blink::mojom::DisplayMode::kBrowser) { |
| debug_data_.Set("!redirection_result", "N/A, btab"); |
| redirection_result_ = NavigationCapturingRedirectionResult::kSameContext; |
| return content::NavigationThrottle::PROCEED; |
| } |
| // browser-tab-app -> standalone-app. This must have a source app id to |
| // ensure that we cannot have a user-modified click go from a regular |
| // browser tab to an app window. |
| CHECK(target_display_mode != blink::mojom::DisplayMode::kBrowser); |
| if (source_browser_app_id_.has_value()) { |
| SetLaunchedAppId(*target_app_id); |
| debug_data_.Set("!redirection_result", "btab to app"); |
| ReparentToAppBrowser(web_contents_for_navigation, *target_app_id, |
| target_display_mode, final_url); |
| redirection_result_ = |
| NavigationCapturingRedirectionResult::kReparentBrowserTabToApp; |
| return content::NavigationThrottle::PROCEED; |
| } |
| } |
| |
| // Handle the last user-modified correction, where a user-modified click from |
| // an app went to the browser, but needs to be reparented back into an app. |
| // See |
| // https://bit.ly/pwa-navigation-capturing?tab=t.0#bookmark=id.ugh0e993wsl8 |
| // for more information. |
| if (initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kNewTabRedirectionEligible && |
| is_user_modified_click() && source_browser_app_id_.has_value()) { |
| // As per the UX direction in the doc, NEW_BACKGROUND_TAB only creates a |
| // new app window for an app when coming from the same app window. |
| // Otherwise, only NEW_WINDOW can create a new app window when coming from |
| // an app. |
| if ((disposition_ == WindowOpenDisposition::NEW_BACKGROUND_TAB && |
| is_source_app_matching_final_target) || |
| (disposition_ == WindowOpenDisposition::NEW_WINDOW)) { |
| // browser-tab -> browser-tab-app. |
| if (target_display_mode == blink::mojom::DisplayMode::kBrowser) { |
| SetLaunchedAppId(*target_app_id, /*force_iph_off=*/true); |
| debug_data_.Set("!redirection_result", "N/A, btab"); |
| redirection_result_ = |
| NavigationCapturingRedirectionResult::kSameContext; |
| return content::NavigationThrottle::PROCEED; |
| } |
| // browser-tab -> standalone app |
| SetLaunchedAppId(*target_app_id); |
| debug_data_.Set("!redirection_result", "btab to app"); |
| ReparentToAppBrowser(web_contents_for_navigation, *target_app_id, |
| target_display_mode, final_url); |
| redirection_result_ = |
| NavigationCapturingRedirectionResult::kReparentBrowserTabToApp; |
| return content::NavigationThrottle::PROCEED; |
| } |
| } |
| |
| // All other user-modified cases should do the default thing and navigate the |
| // existing container. |
| if (is_user_modified_click()) { |
| debug_data_.Set("!redirection_result", "N/A"); |
| redirection_result_ = NavigationCapturingRedirectionResult::kSameContext; |
| return content::NavigationThrottle::PROCEED; |
| } |
| |
| ClientModeAndBrowser client_mode_and_browser = |
| GetEffectiveClientModeAndBrowser(*target_app_id, final_url); |
| |
| // After this point: |
| // - The navigation is non-user-modified. |
| // - This is a top-level browsing context. |
| // - The first navigation app_id doesn't match the target app_id (as per "Same |
| // first navigation state" case above). |
| |
| // Handle all cases where the initial navigation was captured, and that now |
| // needs to be corrected. See the table at |
| // bit.ly/pwa-navigation-handling-dd?tab=t.0#bookmark=id.hnvzj4iwiviz |
| |
| // First, address the 'navigate-new' or 'browser' initial capture, where the |
| // final state is also an effective 'navigate-new'. |
| if (client_mode_and_browser.effective_client_mode == |
| LaunchHandler::ClientMode::kNavigateNew && |
| (initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kNewTabRedirectionEligible || |
| initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kNewAppWindow || |
| initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kNewAppBrowserTab)) { |
| // Handle all cases that result in a standalone app. |
| // (browser tab, browser-tab-app, or standalone-app -> standalone-app) |
| if (target_display_mode != blink::mojom::DisplayMode::kBrowser) { |
| SetLaunchedAppId(*target_app_id); |
| debug_data_.Set("!redirection_result", "app"); |
| ReparentToAppBrowser(web_contents_for_navigation, *target_app_id, |
| target_display_mode, final_url); |
| redirection_result_ = |
| NavigationCapturingRedirectionResult::kAppWindowOpened; |
| return content::NavigationThrottle::PROCEED; |
| } |
| // Handle all cases that result in a browser-tab-app. |
| // (browser tab, browser-tab-app, or standalone-app -> browser-tab-app) |
| CHECK(target_display_mode == blink::mojom::DisplayMode::kBrowser); |
| SetLaunchedAppId(*target_app_id, /*force_iph_off=*/true); |
| if (initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kNewAppWindow) { |
| debug_data_.Set("!redirection_result", "btab"); |
| ReparentWebContentsToTabbedBrowser(web_contents_for_navigation, |
| disposition_, |
| navigation_params_browser_); |
| redirection_result_ = |
| NavigationCapturingRedirectionResult::kAppBrowserTabOpened; |
| } |
| return content::NavigationThrottle::PROCEED; |
| } |
| |
| // Only proceed from now on if the final app can be capturable depending on |
| // the result of the initial navigation handling. This involves only 2 |
| // use-cases, where the intermediary result is either a browser tab, or an app |
| // window that opened as a result of a capturable navigation. |
| bool final_navigation_can_be_capturable = |
| InitialResultWasCaptured() || |
| initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kNewTabRedirectionEligible; |
| if (!final_navigation_can_be_capturable) { |
| debug_data_.Set("!redirection_result", "N/A, not capturable"); |
| redirection_result_ = NavigationCapturingRedirectionResult::kNotCapturable; |
| return content::NavigationThrottle::PROCEED; |
| } |
| |
| // Handle the use-case where the target_app_id has a launch handling mode of |
| // kFocusExisting or kNavigateExisting. In both cases this navigation gets |
| // aborted, and in some cases the web contents being navigated gets closed. |
| if (client_mode_and_browser.effective_client_mode == |
| LaunchHandler::ClientMode::kFocusExisting || |
| client_mode_and_browser.effective_client_mode == |
| LaunchHandler::ClientMode::kNavigateExisting) { |
| CHECK(client_mode_and_browser.browser); |
| CHECK(client_mode_and_browser.tab_index.has_value()); |
| CHECK_NE(*client_mode_and_browser.tab_index, -1); |
| |
| FocusAppContainer(client_mode_and_browser.browser, |
| *client_mode_and_browser.tab_index); |
| |
| content::WebContents* pre_existing_contents = |
| client_mode_and_browser.browser->tab_strip_model()->GetWebContentsAt( |
| *client_mode_and_browser.tab_index); |
| CHECK(pre_existing_contents); |
| CHECK_NE(pre_existing_contents, web_contents_for_navigation); |
| |
| bool is_web_app_browser = |
| WebAppBrowserController::IsWebApp(client_mode_and_browser.browser); |
| |
| if (client_mode_and_browser.effective_client_mode == |
| LaunchHandler::ClientMode::kNavigateExisting) { |
| content::OpenURLParams params = |
| content::OpenURLParams::FromNavigationHandle(navigation_handle()); |
| |
| // Reset the frame_tree_node_id to make sure we're navigating the main |
| // frame in the target web contents. |
| params.frame_tree_node_id = {}; |
| |
| pre_existing_contents->OpenURL( |
| params, |
| base::BindOnce( |
| [](const webapps::AppId& target_app_id, |
| base::TimeTicks time_navigation_started, |
| content::NavigationHandle& navigation_handle) { |
| WebAppLaunchNavigationHandleUserData::CreateForNavigationHandle( |
| navigation_handle, target_app_id, /*force_iph_off=*/false, |
| time_navigation_started); |
| }, |
| *target_app_id, time_navigation_started_)); |
| debug_data_.Set("!redirection_result", "cancel, navigate-existing"); |
| redirection_result_ = |
| is_web_app_browser |
| ? NavigationCapturingRedirectionResult::kNavigateExistingAppWindow |
| : NavigationCapturingRedirectionResult:: |
| kNavigateExistingAppBrowserTab; |
| } else { |
| // Perform post navigation operations, like recording app launch metrics, |
| // or showing the navigation capturing IPH. |
| CHECK(!time_navigation_started_.is_null()); |
| EnqueueLaunchParams(pre_existing_contents, *target_app_id, final_url, |
| /*wait_for_navigation_to_complete=*/false, |
| time_navigation_started_); |
| MaybeShowNavigationCaptureIph(*target_app_id, &*profile_, |
| client_mode_and_browser.browser); |
| RecordLaunchMetrics(*target_app_id, |
| apps::LaunchContainer::kLaunchContainerWindow, |
| apps::LaunchSource::kFromNavigationCapturing, |
| final_url, pre_existing_contents); |
| RecordNavigationCapturingDisplayModeMetrics( |
| *target_app_id, pre_existing_contents, !is_web_app_browser); |
| debug_data_.Set("!redirection_result", "cancel, focus-existing"); |
| redirection_result_ = |
| is_web_app_browser |
| ? NavigationCapturingRedirectionResult::kFocusExistingAppWindow |
| : NavigationCapturingRedirectionResult:: |
| kFocusExistingAppBrowserTab; |
| } |
| |
| // Close the old tab or app window, if it was created as part of the current |
| // navigation to mimic the behavior where the redirected url matches an |
| // outcome without redirection. Any residual app windows or tabs that were |
| // there before the current navigation started shouldn't be closed. |
| if (initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kNewAppWindow || |
| initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kNewAppBrowserTab || |
| initial_nav_handling_result_ == |
| NavigationCapturingInitialResult::kNewTabRedirectionEligible) { |
| debug_data_.Set("redirection_closed_page", true); |
| web_contents_for_navigation->ClosePage(); |
| } |
| return content::NavigationThrottle::CANCEL; |
| } |
| |
| debug_data_.Set("!redirection_result", "Noop2"); |
| redirection_result_ = NavigationCapturingRedirectionResult::kSameContext; |
| return content::NavigationThrottle::PROCEED; |
| } |
| |
| void NavigationCapturingProcess::OnAttachedToNavigationHandle() { |
| CHECK(navigation_handle()); |
| CHECK(IsHandledByNavigationCapturing()); |
| if (!launched_app_id_) { |
| return; |
| } |
| |
| web_app::WebAppLaunchNavigationHandleUserData::CreateForNavigationHandle( |
| *navigation_handle(), *launched_app_id_, force_iph_off_, |
| time_navigation_started_); |
| } |
| |
| bool NavigationCapturingProcess:: |
| IsNavigationCapturingReimplExperimentEnabled() { |
| if (first_navigation_app_display_mode_ && |
| !WebAppRegistrar::IsSupportedDisplayModeForNavigationCapture( |
| *first_navigation_app_display_mode_)) { |
| return false; |
| } |
| // Enabling the generic flag turns it on for all navigations. |
| if (apps::features::IsNavigationCapturingReimplEnabled()) { |
| if (!features::kForcedOffCapturingAppsOnFirstNavigation.Get().empty() && |
| first_navigation_app_id_.has_value()) { |
| std::vector<std::string> forced_capturing_off_app_ids = base::SplitString( |
| features::kForcedOffCapturingAppsOnFirstNavigation.Get(), ",", |
| base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| for (const std::string& forced_capturing_off_app_id : |
| forced_capturing_off_app_ids) { |
| if (first_navigation_app_id_ == forced_capturing_off_app_id) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Check application-specific flags. |
| if (first_navigation_app_id_.has_value() && |
| ::web_app::ChromeOsWebAppExperiments:: |
| IsNavigationCapturingReimplEnabledForTargetApp( |
| *first_navigation_app_id_)) { |
| return true; |
| } |
| if (source_browser_app_id_.has_value() && |
| ::web_app::ChromeOsWebAppExperiments:: |
| IsNavigationCapturingReimplEnabledForSourceApp( |
| *source_browser_app_id_, navigation_params_url_)) { |
| return true; |
| } |
| #endif |
| |
| return false; |
| } |
| |
| NavigationCapturingProcess::ClientModeAndBrowser |
| NavigationCapturingProcess::GetEffectiveClientModeAndBrowser( |
| const webapps::AppId& app_id, |
| const GURL& target_url) { |
| WebAppProvider* provider = WebAppProvider::GetForWebApps(&*profile_); |
| CHECK(provider); |
| web_app::WebAppRegistrar& registrar = provider->registrar_unsafe(); |
| |
| ClientModeAndBrowser result; |
| result.effective_client_mode = registrar.GetAppById(app_id) |
| ->launch_handler() |
| .value_or(LaunchHandler()) |
| .parsed_client_mode(); |
| if (result.effective_client_mode == LaunchHandler::ClientMode::kAuto) { |
| result.effective_client_mode = LaunchHandler::ClientMode::kNavigateNew; |
| } |
| |
| blink::mojom::DisplayMode requested_display_mode = |
| registrar.GetAppEffectiveDisplayMode(app_id); |
| |
| // Opening in non-browser-tab requires OS integration. Since os integration |
| // cannot be triggered synchronously, treat this as opening in browser. |
| if (registrar.GetInstallState(app_id) == |
| proto::INSTALLED_WITHOUT_OS_INTEGRATION) { |
| requested_display_mode = blink::mojom::DisplayMode::kBrowser; |
| } |
| |
| // If the developer has an in-scope link that opens a new top level browsing |
| // in their own page, then force the client mode to be navigate-new. To |
| // detect this, we consider both the app_id that controls the referrer url |
| // as well as the app id of the tab that initiated the navigation. |
| if (source_tab_app_id_ == app_id) { |
| result.effective_client_mode = LaunchHandler::ClientMode::kNavigateNew; |
| } |
| |
| if (result.effective_client_mode == |
| LaunchHandler::ClientMode::kNavigateExisting || |
| result.effective_client_mode == |
| LaunchHandler::ClientMode::kFocusExisting) { |
| bool for_focus_existing = result.effective_client_mode == |
| LaunchHandler::ClientMode::kFocusExisting; |
| // For navigate and focus existing find an existing tab for this app, |
| // depending on the display mode requested. |
| std::optional<AppBrowserController::BrowserAndTabIndex> existing_app_host; |
| switch (requested_display_mode) { |
| case blink::mojom::DisplayMode::kUndefined: |
| case blink::mojom::DisplayMode::kBrowser: |
| // If a populated browser exists with an applicable tab for |
| // focus-existing or navigate-existing, use that instead of a another |
| // existing browser. Note: This could mean that the provided browser is |
| // not a 'normal' and could be an app browser. It seems fine to |
| // respect this. |
| if (navigation_params_browser_) { |
| std::optional<int> tab_index = |
| AppBrowserController::FindTabIndexForApp( |
| navigation_params_browser_, app_id, for_focus_existing); |
| if (tab_index.has_value()) { |
| existing_app_host = {.browser = navigation_params_browser_, |
| .tab_index = *tab_index}; |
| break; |
| } |
| } |
| existing_app_host = |
| AppBrowserController::FindTopLevelBrowsingContextForWebApp( |
| *profile_, app_id, Browser::TYPE_NORMAL, for_focus_existing); |
| break; |
| case blink::mojom::DisplayMode::kMinimalUi: |
| case blink::mojom::DisplayMode::kStandalone: |
| case blink::mojom::DisplayMode::kWindowControlsOverlay: |
| case blink::mojom::DisplayMode::kBorderless: |
| case blink::mojom::DisplayMode::kTabbed: { |
| // First try to choose an existing app host based on whether the |
| // params.browser is populated and belongs to the same `app_id`. |
| // If that is not found, start looking into all active app browsers. |
| if (navigation_params_browser_ && |
| WebAppBrowserController::IsForWebApp(navigation_params_browser_, |
| app_id)) { |
| std::optional<int> tab_index = |
| AppBrowserController::FindTabIndexForApp( |
| navigation_params_browser_, app_id, for_focus_existing); |
| if (tab_index.has_value()) { |
| existing_app_host = {.browser = navigation_params_browser_, |
| .tab_index = *tab_index}; |
| break; |
| } |
| } |
| |
| using HomeTabScope = AppBrowserController::HomeTabScope; |
| HomeTabScope home_tab_scope = HomeTabScope::kDontCare; |
| if (requested_display_mode == blink::mojom::DisplayMode::kTabbed && |
| result.effective_client_mode == |
| LaunchHandler::ClientMode::kNavigateExisting) { |
| home_tab_scope = registrar.IsUrlInHomeTabScope(target_url, app_id) |
| ? HomeTabScope::kInScope |
| : HomeTabScope::kOutOfScope; |
| } |
| existing_app_host = |
| AppBrowserController::FindTopLevelBrowsingContextForWebApp( |
| *profile_, app_id, Browser::TYPE_APP, for_focus_existing, |
| home_tab_scope); |
| // If no app tab was found, fall back to looking for a regular browser |
| // tab. |
| if (!existing_app_host) { |
| existing_app_host = |
| AppBrowserController::FindTopLevelBrowsingContextForWebApp( |
| *profile_, app_id, Browser::TYPE_NORMAL, for_focus_existing); |
| } |
| break; |
| } |
| case blink::mojom::DisplayMode::kFullscreen: |
| case blink::mojom::DisplayMode::kPictureInPicture: |
| NOTREACHED(); |
| } |
| |
| if (existing_app_host.has_value()) { |
| CHECK(existing_app_host->browser); |
| CHECK_NE(existing_app_host->tab_index, -1); |
| result.browser = existing_app_host->browser; |
| result.tab_index = existing_app_host->tab_index; |
| return result; |
| } |
| // If no tab was found to focus or navigate, we'll need to open and |
| // navigate a new tab instead. |
| result.effective_client_mode = LaunchHandler::ClientMode::kNavigateNew; |
| } |
| |
| CHECK_EQ(result.effective_client_mode, |
| LaunchHandler::ClientMode::kNavigateNew); |
| result.tab_index = std::nullopt; |
| switch (requested_display_mode) { |
| case blink::mojom::DisplayMode::kUndefined: |
| case blink::mojom::DisplayMode::kBrowser: |
| // For kBrowser apps, an explicitly specific browser to navigate in |
| // should override what browser we might otherwise use for the profile. |
| if (navigation_params_browser_ && |
| navigation_params_browser_->is_type_normal()) { |
| result.browser = navigation_params_browser_; |
| } else { |
| result.browser = FindNormalBrowser(*profile_); |
| } |
| break; |
| case blink::mojom::DisplayMode::kMinimalUi: |
| case blink::mojom::DisplayMode::kStandalone: |
| case blink::mojom::DisplayMode::kWindowControlsOverlay: |
| case blink::mojom::DisplayMode::kBorderless: |
| // Non-tabbed standalone modes do not support opening a new tab in an |
| // existing browser. So never return a browser in this case. |
| break; |
| case blink::mojom::DisplayMode::kTabbed: |
| // TODO(crbug.com/403587716): Add tests for this case. Test should mimic |
| // opening two app windows and prioritizing the one that gets passed in |
| // NavigateParams. |
| if (navigation_params_browser_ && |
| WebAppBrowserController::IsForWebApp(navigation_params_browser_, |
| app_id) && |
| navigation_params_browser_->app_controller()->has_tab_strip()) { |
| result.browser = navigation_params_browser_; |
| break; |
| } |
| result.browser = AppBrowserController::FindForWebApp(*profile_, app_id); |
| // If somehow we found a browser that doesn't have a tab strip (which |
| // might be possible if the manifest updated while a window is open), |
| // don't return it to use for new tabs. |
| if (result.browser && |
| !result.browser->app_controller()->has_tab_strip()) { |
| result.browser = nullptr; |
| } |
| break; |
| case blink::mojom::DisplayMode::kFullscreen: |
| case blink::mojom::DisplayMode::kPictureInPicture: |
| NOTREACHED(); |
| } |
| |
| return result; |
| } |
| |
| BrowserAndTabOverride NavigationCapturingProcess::CapturingDisabled() { |
| // Don't record debug information for ALL navigations unless expensive DCHECKs |
| // are enabled. |
| #if EXPENSIVE_DCHECKS_ARE_ON() |
| debug_data_.Set("!result", "capturing disabled"); |
| #else |
| debug_data_.clear(); |
| #endif |
| CHECK_EQ(state_, PipelineState::kCreated); |
| state_ = PipelineState::kInitialOverrideCalculated; |
| initial_nav_handling_result_ = NavigationCapturingInitialResult::kNotHandled; |
| return std::nullopt; |
| } |
| |
| BrowserAndTabOverride NavigationCapturingProcess::CancelInitialNavigation( |
| NavigationCapturingInitialResult result) { |
| debug_data_.Set("!result", "cancel navigation"); |
| CHECK_EQ(state_, PipelineState::kCreated); |
| state_ = PipelineState::kInitialOverrideCalculated; |
| initial_nav_handling_result_ = result; |
| return {{nullptr, -1}}; |
| } |
| |
| BrowserAndTabOverride NavigationCapturingProcess::NoCapturingOverrideBrowser( |
| Browser* browser) { |
| debug_data_.Set("!result", "no capturing, override browser"); |
| CHECK_EQ(state_, PipelineState::kCreated); |
| state_ = PipelineState::kInitialOverrideCalculated; |
| initial_nav_handling_result_ = |
| NavigationCapturingInitialResult::kOverrideBrowser; |
| return {{browser, -1}}; |
| } |
| |
| BrowserAndTabOverride NavigationCapturingProcess::AuxiliaryContext() { |
| initial_nav_handling_result_ = |
| NavigationCapturingInitialResult::kAuxiliaryContextAppBrowserTab; |
| // Don't record debug information for ALL navigations unless expensive DCHECKs |
| // are enabled. |
| #if EXPENSIVE_DCHECKS_ARE_ON() |
| debug_data_.Set("!result", "auxiliary context"); |
| #else |
| debug_data_.clear(); |
| #endif |
| CHECK_EQ(state_, PipelineState::kCreated); |
| state_ = PipelineState::kInitialOverrideCalculated; |
| return std::nullopt; |
| } |
| |
| BrowserAndTabOverride NavigationCapturingProcess::AuxiliaryContextInAppWindow( |
| Browser* app_browser) { |
| CHECK(app_browser->app_controller()); |
| initial_nav_handling_result_ = |
| NavigationCapturingInitialResult::kAuxiliaryContextAppWindow; |
| if (first_navigation_app_id_.has_value()) { |
| SetLaunchedAppId(*first_navigation_app_id_); |
| } |
| debug_data_.Set("!result", "auxiliary context in app window"); |
| CHECK_EQ(state_, PipelineState::kCreated); |
| state_ = PipelineState::kInitialOverrideCalculated; |
| return {{app_browser, -1}}; |
| } |
| |
| BrowserAndTabOverride |
| NavigationCapturingProcess::NoInitialActionRedirectionHandlingEligible() { |
| initial_nav_handling_result_ = |
| NavigationCapturingInitialResult::kNewTabRedirectionEligible; |
| // Don't record debug information for ALL navigations unless expensive DCHECKs |
| // are enabled. |
| // TODO(https://crbug.com/351775835): Consider not erasing debug data until we |
| // know the redirect wasn't navigation captured either. |
| #if EXPENSIVE_DCHECKS_ARE_ON() |
| debug_data_.Set("!result", |
| "no initial action, redirection handling eligible"); |
| #else |
| debug_data_.clear(); |
| #endif |
| CHECK_EQ(state_, PipelineState::kCreated); |
| state_ = PipelineState::kInitialOverrideCalculated; |
| return std::nullopt; |
| } |
| |
| BrowserAndTabOverride NavigationCapturingProcess::ForcedNewAppContext( |
| blink::mojom::DisplayMode app_display_mode, |
| Browser* host_browser) { |
| CHECK(first_navigation_app_id_.has_value()); |
| CHECK(WebAppRegistrar::IsSupportedDisplayModeForNavigationCapture( |
| app_display_mode)); |
| CHECK((app_display_mode != blink::mojom::DisplayMode::kBrowser) == |
| (!!host_browser->app_controller())); |
| CHECK(disposition_ == WindowOpenDisposition::NEW_BACKGROUND_TAB || |
| disposition_ == WindowOpenDisposition::NEW_WINDOW); |
| CHECK(is_user_modified_click()); |
| initial_nav_handling_result_ = |
| app_display_mode == DisplayMode::kBrowser |
| ? NavigationCapturingInitialResult::kForcedContextAppBrowserTab |
| : NavigationCapturingInitialResult::kForcedContextAppWindow; |
| // Do not show iph when opening browser-tab-apps in a new browser tab, as |
| // this matches what is 'normal' - clicking on a link opens a new browser |
| // tab. |
| SetLaunchedAppId(*first_navigation_app_id_, |
| /*force_iph_off=*/app_display_mode == DisplayMode::kBrowser); |
| debug_data_.Set("!result", "forced new app context"); |
| CHECK_EQ(state_, PipelineState::kCreated); |
| state_ = PipelineState::kInitialOverrideCalculated; |
| return {{host_browser, -1}}; |
| } |
| |
| BrowserAndTabOverride NavigationCapturingProcess::CapturedNewClient( |
| blink::mojom::DisplayMode app_display_mode, |
| Browser* host_browser) { |
| SCOPED_CRASH_KEY_STRING1024("crbug396028223", "app_display_mode", |
| base::ToString((app_display_mode))); |
| SCOPED_CRASH_KEY_STRING1024( |
| "crbug396028223", "contains_app_controller", |
| base::ToString((!!host_browser->app_controller()))); |
| |
| debug_data_.Set("!result", "captured new client"); |
| SCOPED_CRASH_KEY_STRING1024("crbug396028223", "capturing_debug_info", |
| debug_data_.DebugString()); |
| |
| CHECK(first_navigation_app_id_.has_value()); |
| CHECK(WebAppRegistrar::IsSupportedDisplayModeForNavigationCapture( |
| app_display_mode)); |
| CHECK((app_display_mode != blink::mojom::DisplayMode::kBrowser) == |
| (!!host_browser->app_controller())); |
| CHECK(disposition_ == WindowOpenDisposition::NEW_FOREGROUND_TAB); |
| initial_nav_handling_result_ = |
| app_display_mode == DisplayMode::kBrowser |
| ? NavigationCapturingInitialResult::kNewAppBrowserTab |
| : NavigationCapturingInitialResult::kNewAppWindow; |
| // Do not show iph when opening browser-tab-apps in a new browser tab, as |
| // this matches what is 'normal' - clicking on a link opens a new browser |
| // tab. |
| SetLaunchedAppId(*first_navigation_app_id_, |
| /*force_iph_off=*/app_display_mode == DisplayMode::kBrowser); |
| CHECK_EQ(state_, PipelineState::kCreated); |
| state_ = PipelineState::kInitialOverrideCalculated; |
| return {{host_browser, -1}}; |
| } |
| |
| BrowserAndTabOverride NavigationCapturingProcess::CapturedNavigateExisting( |
| Browser* app_browser, |
| int browser_tab) { |
| CHECK(first_navigation_app_id_.has_value()); |
| CHECK(disposition_ == WindowOpenDisposition::NEW_FOREGROUND_TAB); |
| CHECK(browser_tab != -1); |
| |
| initial_nav_handling_result_ = |
| WebAppBrowserController::IsWebApp(app_browser) |
| ? NavigationCapturingInitialResult::kNavigateExistingAppWindow |
| : NavigationCapturingInitialResult::kNavigateExistingAppBrowserTab; |
| SetLaunchedAppId(*first_navigation_app_id_); |
| debug_data_.Set("!result", "captured navigate existing"); |
| CHECK_EQ(state_, PipelineState::kCreated); |
| state_ = PipelineState::kInitialOverrideCalculated; |
| return {{app_browser, browser_tab}}; |
| } |
| |
| BrowserAndTabOverride NavigationCapturingProcess::CapturedFocusExisting( |
| Browser* browser, |
| int browser_tab, |
| const GURL& url) { |
| CHECK(first_navigation_app_id_.has_value()); |
| const auto& app_id = *first_navigation_app_id_; |
| |
| content::WebContents* contents = |
| browser->tab_strip_model()->GetWebContentsAt(browser_tab); |
| CHECK(contents); |
| FocusAppContainer(browser, browser_tab); |
| |
| CHECK(!time_navigation_started_.is_null()); |
| bool is_current_container_window = WebAppBrowserController::IsWebApp(browser); |
| |
| // Abort the navigation by returning a `nullptr`. Because this means |
| // `OnWebAppNavigationAfterWebContentsCreation` won't be called, enqueue |
| // the launch params instantly and record the debug data. |
| EnqueueLaunchParams(contents, app_id, url, |
| /*wait_for_navigation_to_complete=*/false, |
| time_navigation_started_); |
| |
| MaybeShowNavigationCaptureIph(app_id, &*profile_, browser); |
| |
| // TODO(crbug.com/336371044): Update RecordLaunchMetrics() to also work |
| // with apps that open in a new browser tab. |
| RecordLaunchMetrics(app_id, apps::LaunchContainer::kLaunchContainerWindow, |
| apps::LaunchSource::kFromNavigationCapturing, url, |
| contents); |
| |
| RecordNavigationCapturingDisplayModeMetrics(app_id, contents, |
| !is_current_container_window); |
| |
| return CancelInitialNavigation( |
| is_current_container_window |
| ? NavigationCapturingInitialResult::kFocusExistingAppWindow |
| : NavigationCapturingInitialResult::kFocusExistingAppBrowserTab); |
| } |
| |
| void NavigationCapturingProcess::SetLaunchedAppId( |
| std::optional<webapps::AppId> app_id, |
| bool force_iph_off) { |
| CHECK(IsHandledByNavigationCapturing()); |
| launched_app_id_ = app_id; |
| force_iph_off_ = force_iph_off; |
| debug_data_.Set("!result.launched_app_id", app_id.value_or("<none>")); |
| debug_data_.Set("!result.force_iph_off", force_iph_off); |
| if (!navigation_handle()) { |
| return; |
| } |
| |
| // Always delete the existing user data before optionally recreating new user |
| // data. |
| if (WebAppLaunchNavigationHandleUserData::GetForNavigationHandle( |
| *navigation_handle())) { |
| WebAppLaunchNavigationHandleUserData::DeleteForNavigationHandle( |
| *navigation_handle()); |
| } |
| if (launched_app_id_.has_value()) { |
| OnAttachedToNavigationHandle(); |
| } |
| } |
| |
| bool NavigationCapturingProcess::InitialResultWasCaptured() const { |
| switch (initial_nav_handling_result_) { |
| case NavigationCapturingInitialResult::kNewTabRedirectionEligible: |
| case NavigationCapturingInitialResult::kForcedContextAppWindow: |
| case NavigationCapturingInitialResult::kForcedContextAppBrowserTab: |
| case NavigationCapturingInitialResult::kNotHandled: |
| case NavigationCapturingInitialResult::kAuxiliaryContextAppWindow: |
| case NavigationCapturingInitialResult::kAuxiliaryContextAppBrowserTab: |
| case NavigationCapturingInitialResult::kOverrideBrowser: |
| case NavigationCapturingInitialResult::kNavigationCanceled: |
| return false; |
| case NavigationCapturingInitialResult::kNewAppWindow: |
| case NavigationCapturingInitialResult::kNewAppBrowserTab: |
| case NavigationCapturingInitialResult::kNavigateExistingAppBrowserTab: |
| case NavigationCapturingInitialResult::kNavigateExistingAppWindow: |
| case NavigationCapturingInitialResult::kFocusExistingAppBrowserTab: |
| case NavigationCapturingInitialResult::kFocusExistingAppWindow: |
| return true; |
| } |
| } |
| |
| bool NavigationCapturingProcess::IsHandledByNavigationCapturing() const { |
| switch (initial_nav_handling_result_) { |
| case NavigationCapturingInitialResult::kNewTabRedirectionEligible: |
| case NavigationCapturingInitialResult::kForcedContextAppWindow: |
| case NavigationCapturingInitialResult::kForcedContextAppBrowserTab: |
| case NavigationCapturingInitialResult::kAuxiliaryContextAppWindow: |
| case NavigationCapturingInitialResult::kAuxiliaryContextAppBrowserTab: |
| case NavigationCapturingInitialResult::kNewAppWindow: |
| case NavigationCapturingInitialResult::kNewAppBrowserTab: |
| case NavigationCapturingInitialResult::kNavigateExistingAppBrowserTab: |
| case NavigationCapturingInitialResult::kNavigateExistingAppWindow: |
| case NavigationCapturingInitialResult::kFocusExistingAppBrowserTab: |
| case NavigationCapturingInitialResult::kFocusExistingAppWindow: |
| case NavigationCapturingInitialResult::kNavigationCanceled: |
| return true; |
| case NavigationCapturingInitialResult::kNotHandled: |
| case NavigationCapturingInitialResult::kOverrideBrowser: |
| return false; |
| } |
| } |
| |
| base::Value::Dict& NavigationCapturingProcess::PopulateAndGetDebugData() { |
| debug_data_.Set("!navigation_params_url", |
| navigation_params_url_.possibly_invalid_spec()); |
| debug_data_.Set("navigation_params_browser", |
| base::ToString(navigation_params_browser_.get())); |
| debug_data_.Set("isolated_web_app_navigation", isolated_web_app_navigation_); |
| debug_data_.Set("source_tab_app_id", source_tab_app_id_.value_or("<none>")); |
| debug_data_.Set("disposition", base::ToString(disposition_)); |
| debug_data_.Set("state", base::ToString(state_)); |
| debug_data_.Set("initiating_profile", profile_->GetDebugName()); |
| debug_data_.Set("source_browser_app_id", |
| source_browser_app_id_.value_or("<none>")); |
| debug_data_.Set("is_user_modified_click", is_user_modified_click()); |
| debug_data_.Set("first_navigation_app_id", |
| first_navigation_app_id_.value_or("<none>")); |
| debug_data_.Set("first_navigation_registrar_effective_display_mode_", |
| base::ToString(first_navigation_app_display_mode_.value_or( |
| blink::mojom::DisplayMode::kUndefined))); |
| return debug_data_; |
| } |
| |
| NAVIGATION_HANDLE_USER_DATA_KEY_IMPL(NavigationCapturingProcess); |
| |
| } // namespace web_app |