[go: nahoru, domu]

[apps] Pull out user link capturing throttle logic for cross-platform

This change refactors the user link capturing logic inside of both
the AppsNavigationThrottle and the ChromeOsAppsNavigationThrottle into
the LinkCapturingNavigationThrottle with a Delegate class for per-
platform specialization.

This split divorces the core link capturing logic from both
App Service and web apps systems so it can be bound to either as a
provider of apps with URL scopes.

The dependency graph will be:
                 ChromeContentBrowserClient
                    /        |         \
                   v         |          v
  WebAppLinkCapturing (next) |    AppServiceLinkCapturing
                     \       |       /
                      v      v      v
               LinkCapturingNavigationThrottle

Design doc: go/user-link-capturing-throttle

The next patch will create the throttle used for non-CrOS platforms.

Bug: b/269773608
Change-Id: I693ab4b78bd4ed61ba4e641b5f167d64c5a4969c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4727423
Reviewed-by: Yuichiro Hanada <yhanada@chromium.org>
Reviewed-by: Yilkal Abe <yilkal@chromium.org>
Reviewed-by: Dibyajyoti Pal <dibyapal@chromium.org>
Reviewed-by: Alan Cutter <alancutter@chromium.org>
Commit-Queue: Daniel Murphy <dmurph@chromium.org>
Reviewed-by: Tim Sergeant <tsergeant@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1188097}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 0fddec2..debf3b3 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -3541,8 +3541,6 @@
       "accessibility/invert_bubble_prefs.h",
       "accessibility/live_translate_controller_factory.cc",
       "accessibility/live_translate_controller_factory.h",
-      "apps/intent_helper/apps_navigation_throttle.cc",
-      "apps/intent_helper/apps_navigation_throttle.h",
       "apps/intent_helper/apps_navigation_types.cc",
       "apps/intent_helper/apps_navigation_types.h",
       "apps/intent_helper/intent_picker_auto_display_prefs.cc",
@@ -3555,8 +3553,6 @@
       "apps/intent_helper/intent_picker_helpers.h",
       "apps/intent_helper/intent_picker_internal.cc",
       "apps/intent_helper/intent_picker_internal.h",
-      "apps/intent_helper/page_transition_util.cc",
-      "apps/intent_helper/page_transition_util.h",
       "autocomplete/tab_matcher_desktop.cc",
       "autocomplete/tab_matcher_desktop.h",
       "background/background_contents.cc",
@@ -4491,6 +4487,7 @@
       "//chrome/app/theme:chrome_unscaled_resources_grit",
       "//chrome/app/vector_icons",
       "//chrome/browser/apps/app_service",
+      "//chrome/browser/apps/link_capturing",
       "//chrome/browser/cart:mojo_bindings",
       "//chrome/browser/companion/visual_search",
       "//chrome/browser/enterprise/connectors/analysis:features",
@@ -5637,8 +5634,6 @@
 
   if (is_chromeos) {
     sources += [
-      "apps/intent_helper/chromeos_apps_navigation_throttle.cc",
-      "apps/intent_helper/chromeos_apps_navigation_throttle.h",
       "apps/intent_helper/chromeos_disabled_apps_throttle.cc",
       "apps/intent_helper/chromeos_disabled_apps_throttle.h",
       "apps/intent_helper/chromeos_intent_picker_helpers.cc",
diff --git a/chrome/browser/apps/intent_helper/apps_navigation_throttle.cc b/chrome/browser/apps/intent_helper/apps_navigation_throttle.cc
deleted file mode 100644
index d756d70..0000000
--- a/chrome/browser/apps/intent_helper/apps_navigation_throttle.cc
+++ /dev/null
@@ -1,120 +0,0 @@
-// Copyright 2018 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/apps/intent_helper/apps_navigation_throttle.h"
-
-#include <utility>
-
-#include "chrome/browser/apps/app_service/app_launch_params.h"
-#include "chrome/browser/apps/app_service/app_service_proxy.h"
-#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
-#include "chrome/browser/apps/app_service/browser_app_launcher.h"
-#include "chrome/browser/apps/intent_helper/intent_picker_internal.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_window.h"
-#include "chrome/browser/ui/web_applications/app_browser_controller.h"
-#include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
-#include "chrome/browser/web_applications/web_app.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/common/chrome_features.h"
-#include "content/public/browser/browser_context.h"
-#include "content/public/browser/browser_thread.h"
-#include "content/public/browser/navigation_handle.h"
-#include "content/public/browser/web_contents.h"
-#include "third_party/abseil-cpp/absl/types/optional.h"
-#include "third_party/blink/public/common/features.h"
-#include "ui/gfx/image/image.h"
-#include "url/origin.h"
-
-namespace {
-
-using ThrottleCheckResult = content::NavigationThrottle::ThrottleCheckResult;
-
-}  // namespace
-
-namespace apps {
-
-// static
-std::unique_ptr<content::NavigationThrottle>
-AppsNavigationThrottle::MaybeCreate(content::NavigationHandle* handle) {
-  // Don't handle navigations in subframes or main frames that are in a nested
-  // frame tree (e.g. portals, fenced-frame). We specifically allow
-  // prerendering navigations so that we can destroy the prerender. Opening an
-  // app must only happen when the user intentionally navigates; however, for a
-  // prerender, the prerender-activating navigation doesn't run throttles so we
-  // must cancel it during initial loading to get a standard (non-prerendering)
-  // navigation at link-click-time.
-  if (!handle->IsInPrimaryMainFrame() && !handle->IsInPrerenderedMainFrame())
-    return nullptr;
-
-  content::WebContents* web_contents = handle->GetWebContents();
-  if (!ShouldCheckAppsForUrl(web_contents))
-    return nullptr;
-
-  return std::make_unique<AppsNavigationThrottle>(handle);
-}
-
-AppsNavigationThrottle::AppsNavigationThrottle(
-    content::NavigationHandle* navigation_handle)
-    : content::NavigationThrottle(navigation_handle) {}
-
-AppsNavigationThrottle::~AppsNavigationThrottle() = default;
-
-const char* AppsNavigationThrottle::GetNameForLogging() {
-  return "AppsNavigationThrottle";
-}
-
-ThrottleCheckResult AppsNavigationThrottle::WillStartRequest() {
-  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-  starting_url_ = GetStartingGURL(navigation_handle());
-  return HandleRequest();
-}
-
-ThrottleCheckResult AppsNavigationThrottle::WillRedirectRequest() {
-  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-  return HandleRequest();
-}
-
-bool AppsNavigationThrottle::ShouldCancelNavigation(
-    content::NavigationHandle* handle) {
-  return false;
-}
-
-bool AppsNavigationThrottle::navigate_from_link() const {
-  return navigate_from_link_;
-}
-
-ThrottleCheckResult AppsNavigationThrottle::HandleRequest() {
-  content::NavigationHandle* handle = navigation_handle();
-  // If the navigation won't update the current document, don't check intent for
-  // the navigation.
-  if (handle->IsSameDocument())
-    return content::NavigationThrottle::PROCEED;
-
-  content::WebContents* web_contents = handle->GetWebContents();
-  const GURL& url = handle->GetURL();
-  navigate_from_link_ = IsNavigateFromLink(handle);
-
-  // Do not automatically launch the app if we shouldn't override url loading,
-  // or if we don't have a browser, or we are already in an app browser.
-  if (ShouldOverrideUrlLoading(starting_url_, url) &&
-      !InAppBrowser(web_contents)) {
-    // Handles apps that are automatically launched and the navigation needs to
-    // be cancelled. This only applies on the new intent picker system, because
-    // we don't need to defer the navigation to find out preferred app anymore.
-    if (ShouldCancelNavigation(handle)) {
-      return content::NavigationThrottle::CANCEL_AND_IGNORE;
-    }
-  }
-
-  return content::NavigationThrottle::PROCEED;
-}
-
-}  // namespace apps
diff --git a/chrome/browser/apps/intent_helper/apps_navigation_throttle.h b/chrome/browser/apps/intent_helper/apps_navigation_throttle.h
deleted file mode 100644
index 0971220..0000000
--- a/chrome/browser/apps/intent_helper/apps_navigation_throttle.h
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright 2018 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROME_BROWSER_APPS_INTENT_HELPER_APPS_NAVIGATION_THROTTLE_H_
-#define CHROME_BROWSER_APPS_INTENT_HELPER_APPS_NAVIGATION_THROTTLE_H_
-
-#include <memory>
-#include <vector>
-
-#include "base/gtest_prod_util.h"
-#include "content/public/browser/navigation_throttle.h"
-#include "url/gurl.h"
-
-namespace content {
-class NavigationHandle;
-}  // namespace content
-
-namespace apps {
-
-// Allows canceling a navigation to instead be routed to an installed app.
-class AppsNavigationThrottle : public content::NavigationThrottle {
- public:
-  using ThrottleCheckResult = content::NavigationThrottle::ThrottleCheckResult;
-
-  // Possibly creates a navigation throttle that checks if any installed apps
-  // can handle the URL being navigated to.
-  static std::unique_ptr<content::NavigationThrottle> MaybeCreate(
-      content::NavigationHandle* handle);
-
-  explicit AppsNavigationThrottle(content::NavigationHandle* navigation_handle);
-  AppsNavigationThrottle(const AppsNavigationThrottle&) = delete;
-  AppsNavigationThrottle& operator=(const AppsNavigationThrottle&) = delete;
-  ~AppsNavigationThrottle() override;
-
-  // content::NavigationHandle overrides
-  const char* GetNameForLogging() override;
-  ThrottleCheckResult WillStartRequest() override;
-  ThrottleCheckResult WillRedirectRequest() override;
-
- protected:
-  virtual bool ShouldCancelNavigation(content::NavigationHandle* handle);
-
-  bool navigate_from_link() const;
-
-  GURL starting_url_;
-
- private:
-  ThrottleCheckResult HandleRequest();
-
-  // Keeps track of whether the navigation is coming from a link or not. If the
-  // navigation is not from a link, we will not show the pop up for the intent
-  // picker bubble.
-  bool navigate_from_link_ = false;
-};
-
-}  // namespace apps
-
-#endif  // CHROME_BROWSER_APPS_INTENT_HELPER_APPS_NAVIGATION_THROTTLE_H_
diff --git a/chrome/browser/apps/intent_helper/chromeos_apps_navigation_throttle.cc b/chrome/browser/apps/intent_helper/chromeos_apps_navigation_throttle.cc
deleted file mode 100644
index 25d53cc..0000000
--- a/chrome/browser/apps/intent_helper/chromeos_apps_navigation_throttle.cc
+++ /dev/null
@@ -1,305 +0,0 @@
-// Copyright 2019 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/apps/intent_helper/chromeos_apps_navigation_throttle.h"
-
-#include <memory>
-#include <sstream>
-#include <string>
-#include <utility>
-
-#include "ash/webui/projector_app/public/cpp/projector_app_constants.h"
-#include "base/functional/bind.h"
-#include "base/functional/callback_forward.h"
-#include "base/functional/callback_helpers.h"
-#include "base/memory/values_equivalent.h"
-#include "base/memory/weak_ptr.h"
-#include "base/task/sequenced_task_runner_helpers.h"
-#include "base/task/single_thread_task_runner.h"
-#include "base/time/default_tick_clock.h"
-#include "base/time/tick_clock.h"
-#include "chrome/browser/apps/app_service/app_service_proxy.h"
-#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
-#include "chrome/browser/apps/app_service/launch_utils.h"
-#include "chrome/browser/apps/intent_helper/apps_navigation_types.h"
-#include "chrome/browser/apps/intent_helper/chromeos_intent_picker_helpers.h"
-#include "chrome/browser/apps/intent_helper/intent_picker_internal.h"
-#include "chrome/browser/apps/intent_helper/metrics/intent_handling_metrics.h"
-#include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h"
-#include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h"
-#include "chrome/browser/profiles/profile.h"
-#include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
-#include "chrome/browser/web_applications/web_app_helpers.h"
-#include "chrome/browser/web_applications/web_app_id_constants.h"
-#include "chrome/browser/web_applications/web_app_provider.h"
-#include "chrome/browser/web_applications/web_app_tab_helper.h"
-#include "chrome/common/chrome_features.h"
-#include "components/keep_alive_registry/keep_alive_types.h"
-#include "components/keep_alive_registry/scoped_keep_alive.h"
-#include "components/policy/core/common/policy_pref_names.h"
-#include "components/prefs/pref_service.h"
-#include "components/services/app_service/public/cpp/app_launch_util.h"
-#include "components/services/app_service/public/cpp/app_types.h"
-#include "content/public/browser/navigation_handle.h"
-#include "content/public/browser/web_contents.h"
-#include "ui/display/types/display_constants.h"
-
-namespace apps {
-
-namespace {
-
-using ThrottleCheckResult = content::NavigationThrottle::ThrottleCheckResult;
-
-// Usually we want to only capture navigations from clicking a link. For a
-// subset of apps, we want to capture typing into the omnibox as well.
-bool ShouldOnlyCaptureLinks(const std::vector<std::string>& app_ids) {
-  for (const auto& app_id : app_ids) {
-    if (app_id == ash::kChromeUIUntrustedProjectorSwaAppId) {
-      return false;
-    }
-  }
-  return true;
-}
-
-bool IsSystemWebApp(Profile* profile, const std::string& app_id) {
-  bool is_system_web_app = false;
-  apps::AppServiceProxyFactory::GetForProfile(profile)
-      ->AppRegistryCache()
-      .ForOneApp(app_id, [&is_system_web_app](const apps::AppUpdate& update) {
-        if (update.InstallReason() == apps::InstallReason::kSystem) {
-          is_system_web_app = true;
-        }
-      });
-  return is_system_web_app;
-}
-
-// This function redirects an external untrusted |url| to a privileged trusted
-// one for SWAs, if applicable.
-GURL RedirectUrlIfSwa(Profile* profile,
-                      const std::string& app_id,
-                      const GURL& url,
-                      const base::TickClock* clock) {
-  if (!IsSystemWebApp(profile, app_id)) {
-    return url;
-  }
-
-  // Projector:
-  if (app_id == ash::kChromeUIUntrustedProjectorSwaAppId &&
-      url.GetWithEmptyPath() == GURL(ash::kChromeUIUntrustedProjectorPwaUrl)) {
-    std::string override_url = ash::kChromeUIUntrustedProjectorUrl;
-    if (url.path().length() > 1) {
-      override_url += url.path().substr(1);
-    }
-    std::stringstream ss;
-    // Since ChromeOS doesn't reload an app if the URL doesn't change, the line
-    // below appends a unique timestamp to the URL to force a reload.
-    // TODO(b/211787536): Remove the timestamp after we update the trusted URL
-    // to match the user's navigations through the post message api.
-    ss << override_url << "?timestamp=" << clock->NowTicks();
-
-    if (url.has_query()) {
-      ss << '&' << url.query();
-    }
-
-    GURL result(ss.str());
-    DCHECK(result.is_valid());
-    return result;
-  }
-  // Add redirects for other SWAs above this line.
-
-  // No matching SWAs found, returning original url.
-  return url;
-}
-
-IntentHandlingMetrics::Platform GetMetricsPlatform(AppType app_type) {
-  switch (app_type) {
-    case AppType::kArc:
-      return IntentHandlingMetrics::Platform::ARC;
-    case AppType::kWeb:
-    case AppType::kSystemWeb:
-      return IntentHandlingMetrics::Platform::PWA;
-    case AppType::kUnknown:
-    case AppType::kBuiltIn:
-    case AppType::kCrostini:
-    case AppType::kChromeApp:
-    case AppType::kMacOs:
-    case AppType::kPluginVm:
-    case AppType::kStandaloneBrowser:
-    case AppType::kRemote:
-    case AppType::kBorealis:
-    case AppType::kStandaloneBrowserChromeApp:
-    case AppType::kExtension:
-    case AppType::kStandaloneBrowserExtension:
-    case AppType::kBruschetta:
-      NOTREACHED();
-      return IntentHandlingMetrics::Platform::ARC;
-  }
-}
-
-}  // namespace
-
-// static
-std::unique_ptr<apps::AppsNavigationThrottle>
-ChromeOsAppsNavigationThrottle::MaybeCreate(content::NavigationHandle* handle) {
-  // Don't handle navigations in subframes or main frames that are in a nested
-  // frame tree (e.g. portals, fenced-frame). We specifically allow
-  // prerendering navigations so that we can destroy the prerender. Opening an
-  // app must only happen when the user intentionally navigates; however, for a
-  // prerender, the prerender-activating navigation doesn't run throttles so we
-  // must cancel it during initial loading to get a standard (non-prerendering)
-  // navigation at link-click-time.
-  if (!handle->IsInPrimaryMainFrame() && !handle->IsInPrerenderedMainFrame()) {
-    return nullptr;
-  }
-
-  content::WebContents* web_contents = handle->GetWebContents();
-
-  Profile* profile =
-      Profile::FromBrowserContext(web_contents->GetBrowserContext());
-
-  if (!AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile)) {
-    return nullptr;
-  }
-
-  if (!ShouldCheckAppsForUrl(web_contents)) {
-    return nullptr;
-  }
-
-  return std::make_unique<ChromeOsAppsNavigationThrottle>(handle);
-}
-
-// static
-const base::TickClock* ChromeOsAppsNavigationThrottle::clock_ =
-    base::DefaultTickClock::GetInstance();
-
-// static
-void ChromeOsAppsNavigationThrottle::SetClockForTesting(
-    const base::TickClock* tick_clock) {
-  clock_ = tick_clock;
-}
-
-base::OnceClosure&
-ChromeOsAppsNavigationThrottle::GetLinkCaptureLaunchCallbackForTesting() {
-  static base::NoDestructor<base::OnceClosure> callback;
-  return *callback;
-}
-
-ChromeOsAppsNavigationThrottle::ChromeOsAppsNavigationThrottle(
-    content::NavigationHandle* navigation_handle)
-    : apps::AppsNavigationThrottle(navigation_handle) {}
-
-ChromeOsAppsNavigationThrottle::~ChromeOsAppsNavigationThrottle() = default;
-
-bool ChromeOsAppsNavigationThrottle::ShouldCancelNavigation(
-    content::NavigationHandle* handle) {
-  content::WebContents* web_contents = handle->GetWebContents();
-
-  const GURL& url = handle->GetURL();
-
-  Profile* profile =
-      Profile::FromBrowserContext(web_contents->GetBrowserContext());
-
-  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
-
-  AppIdsToLaunchForUrl app_id_to_launch = FindAppIdsToLaunchForUrl(proxy, url);
-
-  if (app_id_to_launch.candidates.empty()) {
-    return false;
-  }
-
-  if (ShouldOnlyCaptureLinks(app_id_to_launch.candidates) &&
-      !navigate_from_link()) {
-    return false;
-  }
-
-  if (!app_id_to_launch.preferred) {
-    return false;
-  }
-
-  const std::string& preferred_app_id = *app_id_to_launch.preferred;
-  // Only automatically launch supported app types.
-  auto app_type = proxy->AppRegistryCache().GetAppType(preferred_app_id);
-  if (app_type != AppType::kArc && app_type != AppType::kWeb &&
-      !IsSystemWebApp(profile, preferred_app_id)) {
-    return false;
-  }
-
-  // Don't capture if already inside the target app scope.
-  if (app_type == AppType::kWeb &&
-      base::ValuesEquivalent(web_app::WebAppTabHelper::GetAppId(web_contents),
-                             &preferred_app_id)) {
-    return false;
-  }
-
-  // If this is a prerender navigation that would otherwise launch an app, we
-  // must cancel it. We only want to launch an app once the URL is
-  // intentionally navigated to by the user. We cancel the navigation here so
-  // that when the link is clicked, we'll run NavigationThrottles again. If we
-  // leave the prerendering alive, the activating navigation won't run
-  // throttles.
-  if (handle->IsInPrerenderedMainFrame()) {
-    return true;
-  }
-
-  // Browser & profile keep-alives must be used to keep the browser & profile
-  // alive because the old window is required to be closed before the new app is
-  // launched, which will destroy the profile & browser if it is the last
-  // window.
-  // Why close the tab first? The way web contents currently work, closing a tab
-  // in a window will re-activate that window if there are more tabs there. So
-  // if we wait until after the launch completes to close the tab, then it will
-  // cause the old window to come to the front hiding the newly launched app
-  // window.
-  std::unique_ptr<ScopedKeepAlive> browser_keep_alive;
-  std::unique_ptr<ScopedProfileKeepAlive> profile_keep_alive;
-  const GURL& last_committed_url = web_contents->GetLastCommittedURL();
-  if (!last_committed_url.is_valid() || last_committed_url.IsAboutBlank() ||
-      // After clicking a link in various apps (eg gchat), a blank redirect page
-      // is left behind. Remove it to clean up. WasInitiatedByLinkClick()
-      // returns false for links clicked from apps.
-      !handle->WasInitiatedByLinkClick()) {
-    browser_keep_alive = std::make_unique<ScopedKeepAlive>(
-        KeepAliveOrigin::APP_LAUNCH, KeepAliveRestartOption::ENABLED);
-    if (!profile->IsOffTheRecord()) {
-      profile_keep_alive = std::make_unique<ScopedProfileKeepAlive>(
-          profile, ProfileKeepAliveOrigin::kAppWindow);
-    }
-    web_contents->ClosePage();
-  }
-
-  auto launch_source = navigate_from_link() ? LaunchSource::kFromLink
-                                            : LaunchSource::kFromOmnibox;
-  GURL redirected_url =
-      RedirectUrlIfSwa(profile, preferred_app_id, url, clock_);
-  // The tab may have been closed, which runs async and causes the browser
-  // window to be refocused. Post a task to launch the app to ensure launching
-  // happens after the tab closed, otherwise the opened app window might be
-  // inactivated.
-  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
-      FROM_HERE,
-      base::BindOnce(
-          &AppServiceProxy::LaunchAppWithUrl, proxy->GetWeakPtr(),
-          preferred_app_id,
-          GetEventFlags(WindowOpenDisposition::NEW_WINDOW,
-                        /*prefer_container=*/true),
-          redirected_url, launch_source,
-          std::make_unique<WindowInfo>(display::kDefaultDisplayId),
-          base::BindOnce(
-              [](std::unique_ptr<ScopedKeepAlive> browser_keep_alive,
-                 std::unique_ptr<ScopedProfileKeepAlive> profile_keep_alive,
-                 LaunchResult&&) {
-                // Note: This function currently only serves to own the "keep
-                // alive" objects until the launch is complete.
-                if (GetLinkCaptureLaunchCallbackForTesting()) {
-                  std::move(GetLinkCaptureLaunchCallbackForTesting()).Run();
-                }
-              },
-              std::move(browser_keep_alive), std::move(profile_keep_alive))));
-
-  IntentHandlingMetrics::RecordPreferredAppLinkClickMetrics(
-      GetMetricsPlatform(app_type));
-  return true;
-}
-
-}  // namespace apps
diff --git a/chrome/browser/apps/intent_helper/chromeos_apps_navigation_throttle.h b/chrome/browser/apps/intent_helper/chromeos_apps_navigation_throttle.h
deleted file mode 100644
index 58b2cbb..0000000
--- a/chrome/browser/apps/intent_helper/chromeos_apps_navigation_throttle.h
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright 2019 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROME_BROWSER_APPS_INTENT_HELPER_CHROMEOS_APPS_NAVIGATION_THROTTLE_H_
-#define CHROME_BROWSER_APPS_INTENT_HELPER_CHROMEOS_APPS_NAVIGATION_THROTTLE_H_
-
-#include <memory>
-#include <vector>
-
-#include "chrome/browser/apps/intent_helper/apps_navigation_throttle.h"
-#include "content/public/browser/navigation_throttle.h"
-#include "url/gurl.h"
-
-namespace base {
-class TickClock;
-}
-
-namespace content {
-class NavigationHandle;
-}  // namespace content
-
-namespace apps {
-
-// Allows navigation to be routed to an installed app. This throttle supports
-// all type of apps in the Chrome OS platform using App Service.
-class ChromeOsAppsNavigationThrottle : public apps::AppsNavigationThrottle {
- public:
-  // Possibly creates a navigation throttle that checks if any installed apps
-  // can handle the URL being navigated to.
-  static std::unique_ptr<apps::AppsNavigationThrottle> MaybeCreate(
-      content::NavigationHandle* handle);
-
-  // Method intended for testing purposes only.
-  // Set clock used for timing to enable manipulation during tests.
-  static void SetClockForTesting(const base::TickClock* tick_clock);
-
-  static base::OnceClosure& GetLinkCaptureLaunchCallbackForTesting();
-
-  explicit ChromeOsAppsNavigationThrottle(
-      content::NavigationHandle* navigation_handle);
-
-  ChromeOsAppsNavigationThrottle(const ChromeOsAppsNavigationThrottle&) =
-      delete;
-  ChromeOsAppsNavigationThrottle& operator=(
-      const ChromeOsAppsNavigationThrottle&) = delete;
-
-  ~ChromeOsAppsNavigationThrottle() override;
-
- private:
-  bool ShouldCancelNavigation(content::NavigationHandle* handle) override;
-
-  // Used to create a unique timestamped URL to force reload apps.
-  // Points to the base::DefaultTickClock by default.
-  static const base::TickClock* clock_;
-};
-
-}  // namespace apps
-
-#endif  // CHROME_BROWSER_APPS_INTENT_HELPER_CHROMEOS_APPS_NAVIGATION_THROTTLE_H_
diff --git a/chrome/browser/apps/intent_helper/chromeos_disabled_apps_throttle.cc b/chrome/browser/apps/intent_helper/chromeos_disabled_apps_throttle.cc
index ed94987..567afac 100644
--- a/chrome/browser/apps/intent_helper/chromeos_disabled_apps_throttle.cc
+++ b/chrome/browser/apps/intent_helper/chromeos_disabled_apps_throttle.cc
@@ -13,7 +13,11 @@
 #include "chrome/browser/apps/intent_helper/intent_picker_internal.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/policy/system_features_disable_list_policy_handler.h"
+#include "chrome/browser/preloading/prefetch/no_state_prefetch/chrome_no_state_prefetch_contents_delegate.h"
 #include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_finder.h"
+#include "chrome/browser/web_applications/web_app_utils.h"
 #include "chrome/grit/browser_resources.h"
 #include "components/strings/grit/components_strings.h"
 #include "content/public/browser/navigation_handle.h"
@@ -45,6 +49,29 @@
   return webui::GetI18nTemplateHtml(html, strings);
 }
 
+bool ShouldCheckAppsForUrl(content::WebContents* web_contents) {
+  // Do not check apps for url if no apps can be installed, e.g. in incognito.
+  // Do not check apps for a no-state prefetcher navigation.
+  if (!web_app::AreWebAppsUserInstallable(
+          Profile::FromBrowserContext(web_contents->GetBrowserContext())) ||
+      prerender::ChromeNoStatePrefetchContentsDelegate::FromWebContents(
+          web_contents) != nullptr) {
+    return false;
+  }
+
+  // Do not check apps for url if we are already in an app browser.
+  // It is possible that the web contents is not inserted to tab strip
+  // model at this stage (e.g. open url in new tab). So if we cannot
+  // find a browser at this moment, skip the check and this will be handled
+  // in later stage.
+  Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
+  if (browser && (browser->is_type_app() || browser->is_type_app_popup())) {
+    return false;
+  }
+
+  return true;
+}
+
 }  // namespace
 
 // static
diff --git a/chrome/browser/apps/intent_helper/intent_picker_helpers.cc b/chrome/browser/apps/intent_helper/intent_picker_helpers.cc
index 68c406a..ef6319e 100644
--- a/chrome/browser/apps/intent_helper/intent_picker_helpers.cc
+++ b/chrome/browser/apps/intent_helper/intent_picker_helpers.cc
@@ -21,11 +21,13 @@
 #include "chrome/browser/apps/intent_helper/intent_picker_features.h"
 #include "chrome/browser/apps/intent_helper/intent_picker_internal.h"
 #include "chrome/browser/feature_engagement/tracker_factory.h"
+#include "chrome/browser/preloading/prefetch/no_state_prefetch/chrome_no_state_prefetch_contents_delegate.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser_finder.h"
 #include "chrome/browser/ui/browser_window.h"
 #include "chrome/browser/ui/intent_picker_tab_helper.h"
 #include "chrome/browser/ui/web_applications/web_app_launch_utils.h"
+#include "chrome/browser/web_applications/web_app_utils.h"
 #include "components/feature_engagement/public/tracker.h"
 #include "content/public/browser/web_contents.h"
 #include "ui/gfx/favicon_size.h"
@@ -42,6 +44,28 @@
 
 namespace {
 
+bool ShouldConsiderWebContentsForIntentPicker(
+    content::WebContents* web_contents) {
+  if (!web_app::AreWebAppsUserInstallable(
+          Profile::FromBrowserContext(web_contents->GetBrowserContext())) ||
+      prerender::ChromeNoStatePrefetchContentsDelegate::FromWebContents(
+          web_contents) != nullptr) {
+    return false;
+  }
+
+  Profile* profile =
+      Profile::FromBrowserContext(web_contents->GetBrowserContext());
+  if (!AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile)) {
+    return false;
+  }
+
+  Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
+  if (browser && (browser->is_type_app() || browser->is_type_app_popup())) {
+    return false;
+  }
+  return true;
+}
+
 void AppendAppsForUrlSync(
     content::WebContents* web_contents,
     const GURL& url,
@@ -187,14 +211,7 @@
 void GetAppsForIntentPicker(
     content::WebContents* web_contents,
     base::OnceCallback<void(std::vector<IntentPickerAppInfo>)> callback) {
-  if (!ShouldCheckAppsForUrl(web_contents)) {
-    std::move(callback).Run({});
-    return;
-  }
-
-  Profile* profile =
-      Profile::FromBrowserContext(web_contents->GetBrowserContext());
-  if (!AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile)) {
+  if (!ShouldConsiderWebContentsForIntentPicker(web_contents)) {
     std::move(callback).Run({});
     return;
   }
diff --git a/chrome/browser/apps/intent_helper/intent_picker_internal.cc b/chrome/browser/apps/intent_helper/intent_picker_internal.cc
index 7832a8a2..4a7c978 100644
--- a/chrome/browser/apps/intent_helper/intent_picker_internal.cc
+++ b/chrome/browser/apps/intent_helper/intent_picker_internal.cc
@@ -6,62 +6,15 @@
 
 #include <utility>
 
-#include "chrome/browser/apps/intent_helper/page_transition_util.h"
-#include "chrome/browser/preloading/prefetch/no_state_prefetch/chrome_no_state_prefetch_contents_delegate.h"
-#include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser_finder.h"
 #include "chrome/browser/ui/browser_window.h"
-#include "chrome/browser/ui/intent_picker_tab_helper.h"
-#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
-#include "chrome/browser/web_applications/web_app_helpers.h"
-#include "chrome/browser/web_applications/web_app_icon_manager.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_utils.h"
 #include "components/no_state_prefetch/browser/no_state_prefetch_contents.h"
-#include "components/page_load_metrics/browser/page_load_metrics_util.h"
+#include "components/services/app_service/public/cpp/app_types.h"
 #include "content/public/browser/navigation_handle.h"
 #include "content/public/browser/web_contents.h"
-#include "extensions/common/constants.h"
 
 namespace apps {
 
-namespace {
-
-// Returns true if |url| is a known and valid redirector that will redirect a
-// navigation elsewhere.
-bool IsGoogleRedirectorUrl(const GURL& url) {
-  // This currently only check for redirectors on the "google" domain.
-  if (!page_load_metrics::IsGoogleSearchHostname(url))
-    return false;
-
-  return url.path_piece() == "/url" && url.has_query();
-}
-
-}  // namespace
-
-bool ShouldCheckAppsForUrl(content::WebContents* web_contents) {
-  // Do not check apps for url if no apps can be installed, e.g. in incognito.
-  // Do not check apps for a no-state prefetcher navigation.
-  if (!web_app::AreWebAppsUserInstallable(
-          Profile::FromBrowserContext(web_contents->GetBrowserContext())) ||
-      prerender::ChromeNoStatePrefetchContentsDelegate::FromWebContents(
-          web_contents) != nullptr) {
-    return false;
-  }
-
-  // Do not check apps for url if we are already in an app browser.
-  // It is possible that the web contents is not inserted to tab strip
-  // model at this stage (e.g. open url in new tab). So if we cannot
-  // find a browser at this moment, skip the check and this will be handled
-  // in later stage.
-  Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
-  if (browser && (browser->is_type_app() || browser->is_type_app_popup()))
-    return false;
-
-  return true;
-}
-
 void ShowIntentPickerBubbleForApps(content::WebContents* web_contents,
                                    std::vector<IntentPickerAppInfo> apps,
                                    bool show_stay_in_chrome,
@@ -83,82 +36,6 @@
       std::move(callback));
 }
 
-bool InAppBrowser(content::WebContents* web_contents) {
-  Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
-  return !browser || browser->is_type_app() || browser->is_type_app_popup();
-}
-
-// Compares the host name of the referrer and target URL to decide whether
-// the navigation needs to be overridden.
-bool ShouldOverrideUrlLoading(const GURL& previous_url,
-                              const GURL& current_url) {
-  // When the navigation is initiated in a web page where sending a referrer
-  // is disabled, |previous_url| can be empty. In this case, we should open
-  // it in the desktop browser.
-  if (!previous_url.is_valid() || previous_url.is_empty())
-    return false;
-
-  // Also check |current_url| just in case.
-  if (!current_url.is_valid() || current_url.is_empty()) {
-    DVLOG(1) << "Unexpected URL: " << current_url << ", opening it in Chrome.";
-    return false;
-  }
-
-  // Check the scheme for both |previous_url| and |current_url| since an
-  // extension could have referred us (e.g. Google Docs).
-  if (!current_url.SchemeIsHTTPOrHTTPS() ||
-      previous_url.SchemeIs(extensions::kExtensionScheme)) {
-    return false;
-  }
-
-  // Skip URL redirectors that are intermediate pages redirecting towards a
-  // final URL.
-  if (IsGoogleRedirectorUrl(current_url))
-    return false;
-
-  return true;
-}
-
-GURL GetStartingGURL(content::NavigationHandle* navigation_handle) {
-  // This helps us determine a reference GURL for the current NavigationHandle.
-  // This is the order or preference: Referrer > LastCommittedURL >
-  // InitiatorOrigin. InitiatorOrigin *should* only be used on very rare cases,
-  // e.g. when the navigation goes from https: to http: on a new tab, thus
-  // losing the other potential referrers.
-  const GURL referrer_url = navigation_handle->GetReferrer().url;
-  if (referrer_url.is_valid() && !referrer_url.is_empty())
-    return referrer_url;
-
-  const GURL last_committed_url =
-      navigation_handle->GetWebContents()->GetLastCommittedURL();
-  if (last_committed_url.is_valid() && !last_committed_url.is_empty())
-    return last_committed_url;
-
-  const auto& initiator_origin = navigation_handle->GetInitiatorOrigin();
-  return initiator_origin.has_value() ? initiator_origin->GetURL() : GURL();
-}
-
-bool IsGoogleRedirectorUrlForTesting(const GURL& url) {
-  return IsGoogleRedirectorUrl(url);
-}
-
-bool IsNavigateFromLink(content::NavigationHandle* navigation_handle) {
-  // Always handle http(s) <form> submissions in Chrome for two reasons: 1) we
-  // don't have a way to send POST data to ARC, and 2) intercepting http(s) form
-  // submissions is not very important because such submissions are usually
-  // done within the same domain. ShouldOverrideUrlLoading() below filters out
-  // such submissions anyway.
-  constexpr bool kAllowFormSubmit = false;
-
-  ui::PageTransition page_transition = navigation_handle->GetPageTransition();
-
-  return !ShouldIgnoreNavigation(page_transition, kAllowFormSubmit,
-                                 navigation_handle->IsInFencedFrameTree(),
-                                 navigation_handle->HasUserGesture()) &&
-         !navigation_handle->WasStartedFromContextMenu() &&
-         !navigation_handle->IsSameDocument();
-}
-
 void CloseOrGoBack(content::WebContents* web_contents) {
   DCHECK(web_contents);
   if (web_contents->GetController().CanGoBack())
diff --git a/chrome/browser/apps/intent_helper/intent_picker_internal.h b/chrome/browser/apps/intent_helper/intent_picker_internal.h
index 248078dc..829b323 100644
--- a/chrome/browser/apps/intent_helper/intent_picker_internal.h
+++ b/chrome/browser/apps/intent_helper/intent_picker_internal.h
@@ -11,35 +11,19 @@
 #include "components/services/app_service/public/cpp/app_types.h"
 
 namespace content {
-class NavigationHandle;
 class WebContents;
 }  // namespace content
 
-class GURL;
-
 namespace apps {
 
-bool ShouldCheckAppsForUrl(content::WebContents* web_contents);
-
 void ShowIntentPickerBubbleForApps(content::WebContents* web_contents,
                                    std::vector<IntentPickerAppInfo> apps,
                                    bool show_stay_in_chrome,
                                    bool show_remember_selection,
                                    IntentPickerResponse callback);
 
-bool InAppBrowser(content::WebContents* web_contents);
-
-bool ShouldOverrideUrlLoading(const GURL& previous_url,
-                              const GURL& current_url);
-
-GURL GetStartingGURL(content::NavigationHandle* navigation_handle);
-
-bool IsNavigateFromLink(content::NavigationHandle* navigation_handle);
-
 void CloseOrGoBack(content::WebContents* web_contents);
 
-bool IsGoogleRedirectorUrlForTesting(const GURL& url);
-
 PickerEntryType GetPickerEntryType(AppType app_type);
 
 }  // namespace apps
diff --git a/chrome/browser/apps/intent_helper/intent_picker_internal_unittest.cc b/chrome/browser/apps/intent_helper/intent_picker_internal_unittest.cc
deleted file mode 100644
index 69737b45..0000000
--- a/chrome/browser/apps/intent_helper/intent_picker_internal_unittest.cc
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright 2020 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/apps/intent_helper/intent_picker_internal.h"
-
-#include "base/test/gtest_util.h"
-#include "chrome/browser/apps/intent_helper/apps_navigation_types.h"
-#include "testing/gtest/include/gtest/gtest.h"
-#include "url/gurl.h"
-
-namespace apps {
-
-TEST(IntentPickersInternalTest, TestIsGoogleRedirectorUrl) {
-  // Test that redirect urls with different TLDs are still recognized.
-  EXPECT_TRUE(IsGoogleRedirectorUrlForTesting(
-      GURL("https://www.google.com.au/url?q=wathever")));
-  EXPECT_TRUE(IsGoogleRedirectorUrlForTesting(
-      GURL("https://www.google.com.mx/url?q=hotpot")));
-  EXPECT_TRUE(IsGoogleRedirectorUrlForTesting(
-      GURL("https://www.google.co/url?q=query")));
-
-  // Non-google domains shouldn't be used as valid redirect links.
-  EXPECT_FALSE(IsGoogleRedirectorUrlForTesting(
-      GURL("https://www.not-google.com/url?q=query")));
-  EXPECT_FALSE(IsGoogleRedirectorUrlForTesting(
-      GURL("https://www.gooogle.com/url?q=legit_query")));
-
-  // This method only takes "/url" as a valid path, it needs to contain a query,
-  // we don't analyze that query as it will expand later on in the same
-  // throttle.
-  EXPECT_TRUE(IsGoogleRedirectorUrlForTesting(
-      GURL("https://www.google.com/url?q=who_dis")));
-  EXPECT_TRUE(IsGoogleRedirectorUrlForTesting(
-      GURL("http://www.google.com/url?q=who_dis")));
-  EXPECT_FALSE(
-      IsGoogleRedirectorUrlForTesting(GURL("https://www.google.com/url")));
-  EXPECT_FALSE(IsGoogleRedirectorUrlForTesting(
-      GURL("https://www.google.com/link?q=query")));
-  EXPECT_FALSE(
-      IsGoogleRedirectorUrlForTesting(GURL("https://www.google.com/link")));
-}
-
-TEST(IntentPickersInternalTest, TestShouldOverrideUrlLoading) {
-  // If either of two parameters is empty, the function should return false.
-  EXPECT_FALSE(ShouldOverrideUrlLoading(GURL(), GURL("http://a.google.com/")));
-  EXPECT_FALSE(ShouldOverrideUrlLoading(GURL("http://a.google.com/"), GURL()));
-  EXPECT_FALSE(ShouldOverrideUrlLoading(GURL(), GURL()));
-
-  // A navigation to an a url that is neither an http nor https scheme cannot be
-  // override.
-  EXPECT_FALSE(ShouldOverrideUrlLoading(
-      GURL("http://www.a.com"), GURL("chrome-extension://fake_document")));
-  EXPECT_FALSE(ShouldOverrideUrlLoading(
-      GURL("https://www.a.com"), GURL("chrome-extension://fake_document")));
-  EXPECT_FALSE(ShouldOverrideUrlLoading(GURL("http://www.a.com"),
-                                        GURL("chrome://fake_document")));
-  EXPECT_FALSE(ShouldOverrideUrlLoading(GURL("http://www.a.com"),
-                                        GURL("file://fake_document")));
-  EXPECT_FALSE(ShouldOverrideUrlLoading(GURL("https://www.a.com"),
-                                        GURL("chrome://fake_document")));
-  EXPECT_FALSE(ShouldOverrideUrlLoading(GURL("https://www.a.com"),
-                                        GURL("file://fake_document")));
-
-  // A navigation from chrome-extension scheme cannot be overridden.
-  EXPECT_FALSE(ShouldOverrideUrlLoading(
-      GURL("chrome-extension://fake_document"), GURL("http://www.a.com")));
-  EXPECT_FALSE(ShouldOverrideUrlLoading(
-      GURL("chrome-extension://fake_document"), GURL("https://www.a.com")));
-  EXPECT_FALSE(ShouldOverrideUrlLoading(GURL("chrome-extension://fake_a"),
-                                        GURL("chrome-extension://fake_b")));
-
-  // Other navigations can be overridden.
-  EXPECT_TRUE(ShouldOverrideUrlLoading(GURL("http://www.google.com"),
-                                       GURL("http://www.not-google.com/")));
-  EXPECT_TRUE(ShouldOverrideUrlLoading(GURL("http://www.not-google.com"),
-                                       GURL("http://www.google.com/")));
-  EXPECT_TRUE(ShouldOverrideUrlLoading(GURL("http://www.google.com"),
-                                       GURL("http://www.google.com/")));
-  EXPECT_TRUE(ShouldOverrideUrlLoading(GURL("http://a.google.com"),
-                                       GURL("http://b.google.com/")));
-  EXPECT_TRUE(ShouldOverrideUrlLoading(GURL("http://a.not-google.com"),
-                                       GURL("http://b.not-google.com")));
-  EXPECT_TRUE(ShouldOverrideUrlLoading(GURL("chrome://fake_document"),
-                                       GURL("http://www.a.com")));
-  EXPECT_TRUE(ShouldOverrideUrlLoading(GURL("file://fake_document"),
-                                       GURL("http://www.a.com")));
-  EXPECT_TRUE(ShouldOverrideUrlLoading(GURL("chrome://fake_document"),
-                                       GURL("https://www.a.com")));
-  EXPECT_TRUE(ShouldOverrideUrlLoading(GURL("file://fake_document"),
-                                       GURL("https://www.a.com")));
-
-  // A navigation going to a redirect url cannot be overridden, unless there's
-  // no query or the path is not valid.
-  EXPECT_FALSE(ShouldOverrideUrlLoading(
-      GURL("http://www.google.com"), GURL("https://www.google.com/url?q=b")));
-  EXPECT_FALSE(ShouldOverrideUrlLoading(
-      GURL("https://www.a.com"), GURL("https://www.google.com/url?q=a")));
-  EXPECT_TRUE(ShouldOverrideUrlLoading(GURL("https://www.a.com"),
-                                       GURL("https://www.google.com/url")));
-  EXPECT_TRUE(ShouldOverrideUrlLoading(
-      GURL("https://www.a.com"), GURL("https://www.google.com/link?q=a")));
-}
-
-}  // namespace apps
diff --git a/chrome/browser/apps/intent_helper/metrics/intent_handling_metrics.cc b/chrome/browser/apps/intent_helper/metrics/intent_handling_metrics.cc
index 06a2cfb..6a3b1aa 100644
--- a/chrome/browser/apps/intent_helper/metrics/intent_handling_metrics.cc
+++ b/chrome/browser/apps/intent_helper/metrics/intent_handling_metrics.cc
@@ -4,13 +4,13 @@
 
 #include "chrome/browser/apps/intent_helper/metrics/intent_handling_metrics.h"
 
-#include "ash/components/arc/metrics/arc_metrics_constants.h"
 #include "base/containers/contains.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/notreached.h"
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
+#include "ash/components/arc/metrics/arc_metrics_constants.h"
 #include "ash/components/arc/metrics/arc_metrics_service.h"
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
diff --git a/chrome/browser/apps/intent_helper/page_transition_util.cc b/chrome/browser/apps/intent_helper/page_transition_util.cc
deleted file mode 100644
index 810d926..0000000
--- a/chrome/browser/apps/intent_helper/page_transition_util.cc
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2016 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/apps/intent_helper/page_transition_util.h"
-
-#include "base/check_op.h"
-#include "base/types/cxx23_to_underlying.h"
-
-namespace apps {
-
-bool ShouldIgnoreNavigation(ui::PageTransition page_transition,
-                            bool allow_form_submit,
-                            bool is_in_fenced_frame_tree,
-                            bool has_user_gesture) {
-  // Navigations inside fenced frame trees are marked with
-  // PAGE_TRANSITION_AUTO_SUBFRAME in order not to add session history items
-  // (see https://crrev.com/c/3265344). So we only check |has_user_gesture|.
-  if (is_in_fenced_frame_tree) {
-    DCHECK(ui::PageTransitionCoreTypeIs(page_transition,
-                                        ui::PAGE_TRANSITION_AUTO_SUBFRAME));
-    return !has_user_gesture;
-  }
-
-  // Mask out any redirect qualifiers
-  page_transition = MaskOutPageTransition(page_transition,
-                                          ui::PAGE_TRANSITION_IS_REDIRECT_MASK);
-
-  if (!ui::PageTransitionCoreTypeIs(page_transition,
-                                    ui::PAGE_TRANSITION_LINK) &&
-      !(allow_form_submit &&
-        ui::PageTransitionCoreTypeIs(page_transition,
-                                     ui::PAGE_TRANSITION_FORM_SUBMIT))) {
-    // Do not handle the |url| if this event wasn't spawned by the user clicking
-    // on a link.
-    return true;
-  }
-
-  if (base::to_underlying(ui::PageTransitionGetQualifier(page_transition)) !=
-      0) {
-    // Qualifiers indicate that this navigation was the result of a click on a
-    // forward/back button, or typing in the URL bar. Don't handle any of those
-    // types of navigations.
-    return true;
-  }
-
-  return false;
-}
-
-ui::PageTransition MaskOutPageTransition(ui::PageTransition page_transition,
-                                         ui::PageTransition mask) {
-  return ui::PageTransitionFromInt(page_transition & ~mask);
-}
-
-}  // namespace apps
diff --git a/chrome/browser/apps/intent_helper/page_transition_util.h b/chrome/browser/apps/intent_helper/page_transition_util.h
deleted file mode 100644
index 20608a40..0000000
--- a/chrome/browser/apps/intent_helper/page_transition_util.h
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2016 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROME_BROWSER_APPS_INTENT_HELPER_PAGE_TRANSITION_UTIL_H_
-#define CHROME_BROWSER_APPS_INTENT_HELPER_PAGE_TRANSITION_UTIL_H_
-
-#include "ui/base/page_transition_types.h"
-
-namespace apps {
-
-// Returns true if ARC should ignore the navigation with the |page_transition|.
-bool ShouldIgnoreNavigation(ui::PageTransition page_transition,
-                            bool allow_form_submit,
-                            bool is_in_fenced_frame_tree,
-                            bool has_user_gesture);
-
-// Removes |mask| bits from |page_transition|.
-ui::PageTransition MaskOutPageTransition(ui::PageTransition page_transition,
-                                         ui::PageTransition mask);
-
-}  // namespace apps
-
-#endif  // CHROME_BROWSER_APPS_INTENT_HELPER_PAGE_TRANSITION_UTIL_H_
diff --git a/chrome/browser/apps/intent_helper/page_transition_util_unittest.cc b/chrome/browser/apps/intent_helper/page_transition_util_unittest.cc
deleted file mode 100644
index fd7b7b3c..0000000
--- a/chrome/browser/apps/intent_helper/page_transition_util_unittest.cc
+++ /dev/null
@@ -1,191 +0,0 @@
-// Copyright 2016 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/apps/intent_helper/page_transition_util.h"
-#include "testing/gtest/include/gtest/gtest.h"
-#include "ui/base/page_transition_types.h"
-
-namespace apps {
-
-// Tests that ShouldIgnoreNavigation returns false only for
-// PAGE_TRANSITION_LINK when |allow_form_submit| is false and
-// |is_in_fenced_frame_tree| is false.
-TEST(PageTransitionUtilTest, TestShouldIgnoreNavigationWithCoreTypes) {
-  EXPECT_FALSE(
-      ShouldIgnoreNavigation(ui::PAGE_TRANSITION_LINK, false, false, false));
-  EXPECT_TRUE(
-      ShouldIgnoreNavigation(ui::PAGE_TRANSITION_TYPED, false, false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(ui::PAGE_TRANSITION_AUTO_BOOKMARK, false,
-                                     false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(ui::PAGE_TRANSITION_AUTO_SUBFRAME, false,
-                                     false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(ui::PAGE_TRANSITION_MANUAL_SUBFRAME, false,
-                                     false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(ui::PAGE_TRANSITION_GENERATED, false,
-                                     false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false,
-                                     false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(ui::PAGE_TRANSITION_FORM_SUBMIT, false,
-                                     false, false));
-  EXPECT_TRUE(
-      ShouldIgnoreNavigation(ui::PAGE_TRANSITION_RELOAD, false, false, false));
-  EXPECT_TRUE(
-      ShouldIgnoreNavigation(ui::PAGE_TRANSITION_KEYWORD, false, false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(ui::PAGE_TRANSITION_KEYWORD_GENERATED,
-                                     false, false, false));
-
-  static_assert(static_cast<int32_t>(ui::PAGE_TRANSITION_KEYWORD_GENERATED) ==
-                    static_cast<int32_t>(ui::PAGE_TRANSITION_LAST_CORE),
-                "Not all core transition types are covered here");
-}
-
-// Test that ShouldIgnoreNavigation accepts FORM_SUBMIT when |allow_form_submit|
-// is true.
-TEST(PageTransitionUtilTest, TestFormSubmit) {
-  EXPECT_FALSE(ShouldIgnoreNavigation(ui::PAGE_TRANSITION_FORM_SUBMIT, true,
-                                      false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(ui::PAGE_TRANSITION_FORM_SUBMIT, false,
-                                     false, false));
-}
-
-// Tests that ShouldIgnoreNavigation returns true when no qualifiers except
-// client redirect and server redirect are provided when
-// |is_in_fenced_frame_tree| is false.
-TEST(PageTransitionUtilTest, TestShouldIgnoreNavigationWithLinkWithQualifiers) {
-  // The navigation is triggered by Forward or Back button.
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
-                                ui::PAGE_TRANSITION_FORWARD_BACK),
-      false, false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_FORM_SUBMIT |
-                                ui::PAGE_TRANSITION_FORWARD_BACK),
-      false, false, false));
-  // The user used the address bar to trigger the navigation.
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
-                                ui::PAGE_TRANSITION_FROM_ADDRESS_BAR),
-      false, false, false));
-  // The user pressed the Home button.
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
-                                ui::PAGE_TRANSITION_HOME_PAGE),
-      false, false, false));
-  // ARC (for example) opened the link in Chrome.
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
-                                ui::PAGE_TRANSITION_FROM_API),
-      false, false, false));
-  // The navigation is triggered by a client side redirect.
-  EXPECT_FALSE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
-                                ui::PAGE_TRANSITION_CLIENT_REDIRECT),
-      false, false, false));
-  // Also tests the case with 2+ qualifiers.
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
-                                ui::PAGE_TRANSITION_FROM_ADDRESS_BAR |
-                                ui::PAGE_TRANSITION_CLIENT_REDIRECT),
-      false, false, false));
-  // The navigation is triggered by a server side redirect.
-  EXPECT_FALSE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
-                                ui::PAGE_TRANSITION_SERVER_REDIRECT),
-      false, false, false));
-  // Also tests the case with 2+ qualifiers.
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
-                                ui::PAGE_TRANSITION_FROM_ADDRESS_BAR |
-                                ui::PAGE_TRANSITION_SERVER_REDIRECT),
-      false, false, false));
-}
-
-// Just in case, does the same with ui::PAGE_TRANSITION_TYPED.
-TEST(PageTransitionUtilTest,
-     TestShouldIgnoreNavigationWithTypedWithQualifiers) {
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
-                                ui::PAGE_TRANSITION_FORWARD_BACK),
-      false, false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
-                                ui::PAGE_TRANSITION_FORWARD_BACK),
-      false, false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
-                                ui::PAGE_TRANSITION_FROM_ADDRESS_BAR),
-      false, false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
-                                ui::PAGE_TRANSITION_HOME_PAGE),
-      false, false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
-                                ui::PAGE_TRANSITION_FROM_API),
-      false, false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
-                                ui::PAGE_TRANSITION_FROM_ADDRESS_BAR |
-                                ui::PAGE_TRANSITION_CLIENT_REDIRECT),
-      false, false, false));
-}
-
-// Test that ShouldIgnoreNavigation accepts SERVER_REDIRECT and CLIENT_REDIRECT
-// when |is_in_fenced_frame_tree| is false.
-TEST(PageTransitionUtilTest, TestShouldIgnoreNavigationWithClientRedirect) {
-  EXPECT_FALSE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK), false, false,
-      false));
-  EXPECT_FALSE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
-                                ui::PAGE_TRANSITION_CLIENT_REDIRECT),
-      false, false, false));
-  EXPECT_FALSE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
-                                ui::PAGE_TRANSITION_SERVER_REDIRECT),
-      false, false, false));
-  EXPECT_FALSE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
-                                ui::PAGE_TRANSITION_SERVER_REDIRECT |
-                                ui::PAGE_TRANSITION_SERVER_REDIRECT),
-      false, false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
-                                ui::PAGE_TRANSITION_CLIENT_REDIRECT |
-                                ui::PAGE_TRANSITION_HOME_PAGE),
-      false, false, false));
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
-                                ui::PAGE_TRANSITION_HOME_PAGE),
-      false, false, false));
-}
-
-// Test that MaskOutPageTransition correctly remove a qualifier from a given
-// |page_transition|.
-TEST(PageTransitionUtilTest, TestMaskOutPageTransition) {
-  ui::PageTransition page_transition = ui::PageTransitionFromInt(
-      ui::PAGE_TRANSITION_LINK | ui::PAGE_TRANSITION_CLIENT_REDIRECT);
-  EXPECT_EQ(static_cast<int>(ui::PAGE_TRANSITION_LINK),
-            static_cast<int>(MaskOutPageTransition(
-                page_transition, ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
-
-  page_transition = ui::PageTransitionFromInt(
-      ui::PAGE_TRANSITION_LINK | ui::PAGE_TRANSITION_SERVER_REDIRECT);
-  EXPECT_EQ(static_cast<int>(ui::PAGE_TRANSITION_LINK),
-            static_cast<int>(MaskOutPageTransition(
-                page_transition, ui::PAGE_TRANSITION_SERVER_REDIRECT)));
-}
-
-// Test that ShouldIgnoreNavigation accepts iff |has_user_gesture| is true
-// when |is_in_fenced_frame_tree| is true.
-TEST(PageTransitionUtilTest, TestInFencedFrameTree) {
-  EXPECT_TRUE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_AUTO_SUBFRAME), false, true,
-      false));
-  EXPECT_FALSE(ShouldIgnoreNavigation(
-      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_AUTO_SUBFRAME), false, true,
-      true));
-}
-
-}  // namespace apps
diff --git a/chrome/browser/apps/link_capturing/BUILD.gn b/chrome/browser/apps/link_capturing/BUILD.gn
new file mode 100644
index 0000000..e536bfd
--- /dev/null
+++ b/chrome/browser/apps/link_capturing/BUILD.gn
@@ -0,0 +1,47 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//chrome/browser/buildflags.gni")
+
+source_set("link_capturing") {
+  sources = [
+    "link_capturing_navigation_throttle.cc",
+    "link_capturing_navigation_throttle.h",
+  ]
+
+  deps = [
+    "//base",
+    "//chrome/browser/profiles:profile",
+    "//chrome/browser/web_applications",
+    "//components/keep_alive_registry",
+    "//components/page_load_metrics/browser",
+    "//content/public/browser",
+    "//extensions/common",
+    "//third_party/abseil-cpp:absl",
+    "//url",
+  ]
+
+  if (is_chromeos) {
+    sources += [
+      "chromeos_link_capturing_delegate.cc",
+      "chromeos_link_capturing_delegate.h",
+    ]
+    deps += [
+      "//ash/webui/projector_app/public/cpp",
+      "//chrome/browser/apps/app_service",
+    ]
+  }
+}
+
+source_set("unit_tests") {
+  testonly = true
+  sources = [ "link_capturing_navigation_throttle_unittest.cc" ]
+
+  deps = [
+    ":link_capturing",
+    "//testing/gtest",
+    "//ui/base:base",
+    "//url",
+  ]
+}
diff --git a/chrome/browser/apps/link_capturing/OWNERS b/chrome/browser/apps/link_capturing/OWNERS
new file mode 100644
index 0000000..306ad10
--- /dev/null
+++ b/chrome/browser/apps/link_capturing/OWNERS
@@ -0,0 +1,2 @@
+tsergeant@chromium.org
+mxcai@chromium.org
diff --git a/chrome/browser/apps/link_capturing/chromeos_link_capturing_delegate.cc b/chrome/browser/apps/link_capturing/chromeos_link_capturing_delegate.cc
new file mode 100644
index 0000000..dd1ca76
--- /dev/null
+++ b/chrome/browser/apps/link_capturing/chromeos_link_capturing_delegate.cc
@@ -0,0 +1,208 @@
+// Copyright 2023 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/apps/link_capturing/chromeos_link_capturing_delegate.h"
+
+#include "ash/webui/projector_app/public/cpp/projector_app_constants.h"
+#include "base/functional/bind.h"
+#include "base/functional/callback_helpers.h"
+#include "base/memory/values_equivalent.h"
+#include "base/time/default_tick_clock.h"
+#include "base/time/tick_clock.h"
+#include "chrome/browser/apps/app_service/app_service_proxy.h"
+#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
+#include "chrome/browser/apps/app_service/launch_utils.h"
+#include "chrome/browser/apps/intent_helper/metrics/intent_handling_metrics.h"  // nogncheck https://crbug.com/1474116
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/web_applications/web_app_tab_helper.h"
+#include "chrome/browser/web_applications/web_app_utils.h"
+#include "content/public/browser/navigation_handle.h"
+#include "content/public/browser/web_contents.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+
+namespace apps {
+namespace {
+// Usually we want to only capture navigations from clicking a link. For a
+// subset of apps, we want to capture typing into the omnibox as well.
+bool ShouldOnlyCaptureLinks(const std::vector<std::string>& app_ids) {
+  for (const auto& app_id : app_ids) {
+    if (app_id == ash::kChromeUIUntrustedProjectorSwaAppId) {
+      return false;
+    }
+  }
+  return true;
+}
+
+bool IsSystemWebApp(Profile* profile, const std::string& app_id) {
+  bool is_system_web_app = false;
+  apps::AppServiceProxyFactory::GetForProfile(profile)
+      ->AppRegistryCache()
+      .ForOneApp(app_id, [&is_system_web_app](const apps::AppUpdate& update) {
+        if (update.InstallReason() == apps::InstallReason::kSystem) {
+          is_system_web_app = true;
+        }
+      });
+  return is_system_web_app;
+}
+
+// This function redirects an external untrusted |url| to a privileged trusted
+// one for SWAs, if applicable.
+GURL RedirectUrlIfSwa(Profile* profile,
+                      const std::string& app_id,
+                      const GURL& url,
+                      const base::TickClock* clock) {
+  if (!IsSystemWebApp(profile, app_id)) {
+    return url;
+  }
+
+  // Projector:
+  if (app_id == ash::kChromeUIUntrustedProjectorSwaAppId &&
+      url.GetWithEmptyPath() == GURL(ash::kChromeUIUntrustedProjectorPwaUrl)) {
+    std::string override_url = ash::kChromeUIUntrustedProjectorUrl;
+    if (url.path().length() > 1) {
+      override_url += url.path().substr(1);
+    }
+    std::stringstream ss;
+    // Since ChromeOS doesn't reload an app if the URL doesn't change, the line
+    // below appends a unique timestamp to the URL to force a reload.
+    // TODO(b/211787536): Remove the timestamp after we update the trusted URL
+    // to match the user's navigations through the post message api.
+    ss << override_url << "?timestamp=" << clock->NowTicks();
+
+    if (url.has_query()) {
+      ss << '&' << url.query();
+    }
+
+    GURL result(ss.str());
+    DCHECK(result.is_valid());
+    return result;
+  }
+  // Add redirects for other SWAs above this line.
+
+  // No matching SWAs found, returning original url.
+  return url;
+}
+
+IntentHandlingMetrics::Platform GetMetricsPlatform(AppType app_type) {
+  switch (app_type) {
+    case AppType::kArc:
+      return IntentHandlingMetrics::Platform::ARC;
+    case AppType::kWeb:
+    case AppType::kSystemWeb:
+      return IntentHandlingMetrics::Platform::PWA;
+    case AppType::kUnknown:
+    case AppType::kBuiltIn:
+    case AppType::kCrostini:
+    case AppType::kChromeApp:
+    case AppType::kMacOs:
+    case AppType::kPluginVm:
+    case AppType::kStandaloneBrowser:
+    case AppType::kRemote:
+    case AppType::kBorealis:
+    case AppType::kStandaloneBrowserChromeApp:
+    case AppType::kExtension:
+    case AppType::kStandaloneBrowserExtension:
+    case AppType::kBruschetta:
+      NOTREACHED();
+      return IntentHandlingMetrics::Platform::ARC;
+  }
+}
+
+void LaunchApp(base::WeakPtr<AppServiceProxy> proxy,
+               const std::string& app_id,
+               int32_t event_flags,
+               GURL url,
+               LaunchSource launch_source,
+               WindowInfoPtr window_info,
+               AppType app_type,
+               base::OnceClosure callback) {
+  if (!proxy) {
+    std::move(callback).Run();
+    return;
+  }
+
+  proxy->LaunchAppWithUrl(
+      app_id, event_flags, url, launch_source, std::move(window_info),
+      base::IgnoreArgs<LaunchResult&&>(std::move(callback)));
+
+  IntentHandlingMetrics::RecordPreferredAppLinkClickMetrics(
+      GetMetricsPlatform(app_type));
+}
+}  // namespace
+
+// static
+const base::TickClock* ChromeOsLinkCapturingDelegate::clock_ =
+    base::DefaultTickClock::GetInstance();
+
+// static
+void ChromeOsLinkCapturingDelegate::SetClockForTesting(
+    const base::TickClock* tick_clock) {
+  clock_ = tick_clock;
+}
+
+ChromeOsLinkCapturingDelegate::ChromeOsLinkCapturingDelegate() = default;
+ChromeOsLinkCapturingDelegate::~ChromeOsLinkCapturingDelegate() = default;
+
+bool ChromeOsLinkCapturingDelegate::ShouldCancelThrottleCreation(
+    content::NavigationHandle* handle) {
+  content::WebContents* web_contents = handle->GetWebContents();
+  Profile* profile =
+      Profile::FromBrowserContext(web_contents->GetBrowserContext());
+  return !AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile);
+}
+
+absl::optional<apps::LinkCapturingNavigationThrottle::LaunchCallback>
+ChromeOsLinkCapturingDelegate::CreateLinkCaptureLaunchClosure(
+    Profile* profile,
+    content::WebContents* web_contents,
+    const GURL& url,
+    bool is_navigation_from_link) {
+  AppServiceProxy* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
+
+  AppIdsToLaunchForUrl app_id_to_launch = FindAppIdsToLaunchForUrl(proxy, url);
+
+  if (app_id_to_launch.candidates.empty()) {
+    return absl::nullopt;
+  }
+
+  if (ShouldOnlyCaptureLinks(app_id_to_launch.candidates) &&
+      !is_navigation_from_link) {
+    return absl::nullopt;
+  }
+
+  if (!app_id_to_launch.preferred) {
+    return absl::nullopt;
+  }
+
+  const std::string& preferred_app_id = *app_id_to_launch.preferred;
+  // Only automatically launch supported app types.
+  AppType app_type = proxy->AppRegistryCache().GetAppType(preferred_app_id);
+  if (app_type != AppType::kArc && app_type != AppType::kWeb &&
+      !IsSystemWebApp(profile, preferred_app_id)) {
+    return absl::nullopt;
+  }
+
+  // Don't capture if already inside the target app scope.
+  if (app_type == AppType::kWeb &&
+      base::ValuesEquivalent(web_app::WebAppTabHelper::GetAppId(web_contents),
+                             &preferred_app_id)) {
+    return absl::nullopt;
+  }
+
+  auto launch_source = is_navigation_from_link ? LaunchSource::kFromLink
+                                               : LaunchSource::kFromOmnibox;
+  GURL redirected_url =
+      RedirectUrlIfSwa(profile, preferred_app_id, url, clock_);
+
+  // Note: The launch can occur after this object is destroyed, so bind to a
+  // static function.
+  return base::BindOnce(
+      &LaunchApp, proxy->GetWeakPtr(), preferred_app_id,
+      GetEventFlags(WindowOpenDisposition::NEW_WINDOW,
+                    /*prefer_container=*/true),
+      redirected_url, launch_source,
+      std::make_unique<WindowInfo>(display::kDefaultDisplayId), app_type);
+}
+
+}  // namespace apps
diff --git a/chrome/browser/apps/link_capturing/chromeos_link_capturing_delegate.h b/chrome/browser/apps/link_capturing/chromeos_link_capturing_delegate.h
new file mode 100644
index 0000000..c25c742a
--- /dev/null
+++ b/chrome/browser/apps/link_capturing/chromeos_link_capturing_delegate.h
@@ -0,0 +1,52 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_APPS_LINK_CAPTURING_CHROMEOS_LINK_CAPTURING_DELEGATE_H_
+#define CHROME_BROWSER_APPS_LINK_CAPTURING_CHROMEOS_LINK_CAPTURING_DELEGATE_H_
+
+#include "base/memory/weak_ptr.h"
+#include "chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.h"
+
+class GURL;
+class Profile;
+
+namespace base {
+class TickClock;
+}
+
+namespace content {
+class NavigationHandle;
+class WebContents;
+}  // namespace content
+
+namespace apps {
+
+class ChromeOsLinkCapturingDelegate
+    : public apps::LinkCapturingNavigationThrottle::Delegate {
+ public:
+  ChromeOsLinkCapturingDelegate();
+  ~ChromeOsLinkCapturingDelegate() override;
+
+  // Method intended for testing purposes only.
+  // Set clock used for timing to enable manipulation during tests.
+  static void SetClockForTesting(const base::TickClock* tick_clock);
+
+  // apps::LinkCapturingNavigationThrottle::Delegate:
+  bool ShouldCancelThrottleCreation(content::NavigationHandle* handle) override;
+  absl::optional<apps::LinkCapturingNavigationThrottle::LaunchCallback>
+  CreateLinkCaptureLaunchClosure(Profile* profile,
+                                 content::WebContents* web_contents,
+                                 const GURL& url,
+                                 bool is_navigation_from_link) final;
+
+ private:
+  // Used to create a unique timestamped URL to force reload apps.
+  // Points to the base::DefaultTickClock by default.
+  static const base::TickClock* clock_;
+
+  base::WeakPtrFactory<ChromeOsLinkCapturingDelegate> weak_factory_{this};
+};
+}  // namespace apps
+
+#endif  // CHROME_BROWSER_APPS_LINK_CAPTURING_CHROMEOS_LINK_CAPTURING_DELEGATE_H_
diff --git a/chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.cc b/chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.cc
new file mode 100644
index 0000000..cf371c7
--- /dev/null
+++ b/chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.cc
@@ -0,0 +1,342 @@
+// Copyright 2023 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/apps/link_capturing/link_capturing_navigation_throttle.h"
+
+#include <utility>
+
+#include "base/memory/ptr_util.h"
+#include "base/no_destructor.h"
+#include "base/task/single_thread_task_runner.h"
+#include "base/types/cxx23_to_underlying.h"
+#include "chrome/browser/preloading/prefetch/no_state_prefetch/chrome_no_state_prefetch_contents_delegate.h"  // nogncheck https://crbug.com/1474116
+#include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h"  // nogncheck https://crbug.com/1474116
+#include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h"  // nogncheck https://crbug.com/1474116
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/web_applications/web_app_provider.h"
+#include "chrome/browser/web_applications/web_app_ui_manager.h"
+#include "chrome/browser/web_applications/web_app_utils.h"
+#include "components/keep_alive_registry/keep_alive_types.h"
+#include "components/keep_alive_registry/scoped_keep_alive.h"
+#include "components/page_load_metrics/browser/page_load_metrics_util.h"
+#include "content/public/browser/browser_thread.h"
+#include "content/public/browser/navigation_handle.h"
+#include "content/public/browser/web_contents.h"
+#include "extensions/common/constants.h"
+#include "url/origin.h"
+
+namespace apps {
+
+namespace {
+
+using ThrottleCheckResult = content::NavigationThrottle::ThrottleCheckResult;
+
+// Retrieves the 'starting' url for the given navigation handle. This considers
+// the referrer url, last committed url, and the initiator origin.
+GURL GetStartingUrl(content::NavigationHandle* navigation_handle) {
+  // This helps us determine a reference GURL for the current NavigationHandle.
+  // This is the order or preference: Referrer > LastCommittedURL >
+  // InitiatorOrigin. InitiatorOrigin *should* only be used on very rare cases,
+  // e.g. when the navigation goes from https: to http: on a new tab, thus
+  // losing the other potential referrers.
+  const GURL referrer_url = navigation_handle->GetReferrer().url;
+  if (referrer_url.is_valid() && !referrer_url.is_empty()) {
+    return referrer_url;
+  }
+
+  const GURL last_committed_url =
+      navigation_handle->GetWebContents()->GetLastCommittedURL();
+  if (last_committed_url.is_valid() && !last_committed_url.is_empty()) {
+    return last_committed_url;
+  }
+
+  const auto& initiator_origin = navigation_handle->GetInitiatorOrigin();
+  return initiator_origin.has_value() ? initiator_origin->GetURL() : GURL();
+}
+
+// Returns if the navigation appears to be a link navigation, but not from an
+// HTML post form.
+bool IsNavigateFromNonFormNonContextMenuLink(
+    content::NavigationHandle* navigation_handle) {
+  // Always handle http(s) <form> submissions in Chrome for two reasons: 1) we
+  // don't have a way to send POST data to ARC, and 2) intercepting http(s) form
+  // submissions is not very important because such submissions are usually
+  // done within the same domain. ShouldOverrideUrlLoading() below filters out
+  // such submissions anyway.
+  constexpr bool kAllowFormSubmit = false;
+
+  ui::PageTransition page_transition = navigation_handle->GetPageTransition();
+
+  return LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+             page_transition, kAllowFormSubmit,
+             navigation_handle->IsInFencedFrameTree(),
+             navigation_handle->HasUserGesture()) &&
+         !navigation_handle->WasStartedFromContextMenu();
+}
+
+}  // namespace
+
+bool LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+    ui::PageTransition page_transition,
+    bool allow_form_submit,
+    bool is_in_fenced_frame_tree,
+    bool has_user_gesture) {
+  // Navigations inside fenced frame trees are marked with
+  // PAGE_TRANSITION_AUTO_SUBFRAME in order not to add session history items
+  // (see https://crrev.com/c/3265344). So we only check |has_user_gesture|.
+  if (is_in_fenced_frame_tree) {
+    DCHECK(ui::PageTransitionCoreTypeIs(page_transition,
+                                        ui::PAGE_TRANSITION_AUTO_SUBFRAME));
+    return has_user_gesture;
+  }
+
+  // Mask out any redirect qualifiers
+  page_transition = MaskOutPageTransition(page_transition,
+                                          ui::PAGE_TRANSITION_IS_REDIRECT_MASK);
+
+  if (!ui::PageTransitionCoreTypeIs(page_transition,
+                                    ui::PAGE_TRANSITION_LINK) &&
+      !(allow_form_submit &&
+        ui::PageTransitionCoreTypeIs(page_transition,
+                                     ui::PAGE_TRANSITION_FORM_SUBMIT))) {
+    // Do not handle the |url| if this event wasn't spawned by the user clicking
+    // on a link.
+    return false;
+  }
+
+  if (base::to_underlying(ui::PageTransitionGetQualifier(page_transition)) !=
+      0) {
+    // Qualifiers indicate that this navigation was the result of a click on a
+    // forward/back button, or typing in the URL bar. Don't handle any of those
+    // types of navigations.
+    return false;
+  }
+
+  return true;
+}
+
+ui::PageTransition LinkCapturingNavigationThrottle::MaskOutPageTransition(
+    ui::PageTransition page_transition,
+    ui::PageTransition mask) {
+  return ui::PageTransitionFromInt(page_transition & ~mask);
+}
+
+LinkCapturingNavigationThrottle::Delegate::~Delegate() = default;
+
+// static
+std::unique_ptr<content::NavigationThrottle>
+LinkCapturingNavigationThrottle::MaybeCreate(
+    content::NavigationHandle* handle,
+    std::unique_ptr<Delegate> delegate) {
+  // Don't handle navigations in subframes or main frames that are in a nested
+  // frame tree (e.g. portals, fenced-frame). We specifically allow
+  // prerendering navigations so that we can destroy the prerender. Opening an
+  // app must only happen when the user intentionally navigates; however, for a
+  // prerender, the prerender-activating navigation doesn't run throttles so we
+  // must cancel it during initial loading to get a standard (non-prerendering)
+  // navigation at link-click-time.
+  if (!handle->IsInPrimaryMainFrame() && !handle->IsInPrerenderedMainFrame()) {
+    return nullptr;
+  }
+
+  content::WebContents* web_contents = handle->GetWebContents();
+  if (prerender::ChromeNoStatePrefetchContentsDelegate::FromWebContents(
+          web_contents) != nullptr) {
+    return nullptr;
+  }
+
+  Profile* profile =
+      Profile::FromBrowserContext(web_contents->GetBrowserContext());
+  if (!web_app::AreWebAppsUserInstallable(profile)) {
+    return nullptr;
+  }
+
+  // Do not check apps for url if we are already in an app browser.
+  // It is possible that the web contents is not inserted to tab strip
+  // model at this stage (e.g. open url in new tab). So if we cannot
+  // find a browser at this moment, skip the check and this will be handled
+  // in `HandleRequest()`.
+  // This also checks if there is no browser attached to this web-contents yet,
+  // which means this was a middle-mouse-click action, which should not be
+  // captured.
+  // TODO(dmurph): Find a better way to detect middle-clicks.
+  // https://crbug.com/1474984
+  if (web_app::WebAppProvider::GetForWebApps(profile)
+          ->ui_manager()
+          .IsAppAffiliatedWindowOrNone(web_contents)) {
+    return nullptr;
+  }
+
+  return base::WrapUnique(
+      new LinkCapturingNavigationThrottle(handle, std::move(delegate)));
+}
+
+base::OnceClosure&
+LinkCapturingNavigationThrottle::GetLinkCaptureLaunchCallbackForTesting() {
+  static base::NoDestructor<base::OnceClosure> callback;
+  return *callback;
+}
+
+LinkCapturingNavigationThrottle::~LinkCapturingNavigationThrottle() = default;
+
+const char* LinkCapturingNavigationThrottle::GetNameForLogging() {
+  return "LinkCapturingNavigationThrottle";
+}
+
+ThrottleCheckResult LinkCapturingNavigationThrottle::WillStartRequest() {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  starting_url_ = GetStartingUrl(navigation_handle());
+  return HandleRequest();
+}
+
+ThrottleCheckResult LinkCapturingNavigationThrottle::WillRedirectRequest() {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+  return HandleRequest();
+}
+
+// Returns true if |url| is a known and valid redirector that will redirect a
+// navigation elsewhere.
+// static
+bool LinkCapturingNavigationThrottle::IsGoogleRedirectorUrl(const GURL& url) {
+  // This currently only check for redirectors on the "google" domain.
+  if (!page_load_metrics::IsGoogleSearchHostname(url)) {
+    return false;
+  }
+
+  return url.path_piece() == "/url" && url.has_query();
+}
+
+// If the previous url and current url are not the same (AKA a redirection),
+// determines if the redirection should be considered for an app launch. Returns
+// false for redirections where:
+// * `previous_url` is an extension.
+// * `previous_url` is a google redirector.
+// static
+bool LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+    const GURL& previous_url,
+    const GURL& current_url) {
+  // Check the scheme for both |previous_url| and |current_url| since an
+  // extension could have referred us (e.g. Google Docs).
+  if (previous_url.SchemeIs(extensions::kExtensionScheme)) {
+    return false;
+  }
+
+  // Skip URL redirectors that are intermediate pages redirecting towards a
+  // final URL.
+  if (IsGoogleRedirectorUrl(current_url)) {
+    return false;
+  }
+
+  return true;
+}
+
+ThrottleCheckResult LinkCapturingNavigationThrottle::HandleRequest() {
+  content::NavigationHandle* handle = navigation_handle();
+
+  // If the navigation will update the same document, don't consider as a
+  // capturable link.
+  if (handle->IsSameDocument()) {
+    return content::NavigationThrottle::PROCEED;
+  }
+
+  // When the navigation is initiated in a web page where sending a referrer
+  // is disabled, |previous_url| can be empty. In this case, we should open
+  // it in the desktop browser.
+  if (!starting_url_.is_valid()) {
+    return content::NavigationThrottle::PROCEED;
+  }
+
+  const GURL& url = handle->GetURL();
+  if (!url.is_valid()) {
+    DVLOG(1) << "Unexpected URL: " << url << ", opening in Chrome.";
+    return content::NavigationThrottle::PROCEED;
+  }
+
+  // Only http-style schemes are allowed.
+  if (!url.SchemeIsHTTPOrHTTPS()) {
+    return content::NavigationThrottle::PROCEED;
+  }
+
+  content::WebContents* web_contents = handle->GetWebContents();
+  Profile* profile =
+      Profile::FromBrowserContext(web_contents->GetBrowserContext());
+
+  // Check this again, as the tab may have been reparented now.
+  // TODO(dmurph): Find a better way to detect middle clicks.
+  // https://crbug.com/1474984
+  if (web_app::WebAppProvider::GetForWebApps(profile)
+          ->ui_manager()
+          .IsAppAffiliatedWindowOrNone(web_contents)) {
+    return content::NavigationThrottle::PROCEED;
+  }
+
+  if (!ShouldOverrideUrlIfRedirected(starting_url_, url)) {
+    return content::NavigationThrottle::PROCEED;
+  }
+
+  bool is_navigation_from_link =
+      IsNavigateFromNonFormNonContextMenuLink(handle);
+
+  absl::optional<LaunchCallback> launch_link_capture =
+      delegate_->CreateLinkCaptureLaunchClosure(profile, web_contents, url,
+                                                is_navigation_from_link);
+  if (!launch_link_capture.has_value()) {
+    return content::NavigationThrottle::PROCEED;
+  }
+
+  // Browser & profile keep-alives must be used to keep the browser & profile
+  // alive because the old window is required to be closed before the new app is
+  // launched, which will destroy the profile & browser if it is the last
+  // window.
+  // Why close the tab first? The way web contents currently work, closing a tab
+  // in a window will re-activate that window if there are more tabs there. So
+  // if we wait until after the launch completes to close the tab, then it will
+  // cause the old window to come to the front hiding the newly launched app
+  // window.
+  std::unique_ptr<ScopedKeepAlive> browser_keep_alive;
+  std::unique_ptr<ScopedProfileKeepAlive> profile_keep_alive;
+  const GURL& last_committed_url = web_contents->GetLastCommittedURL();
+  if (!last_committed_url.is_valid() || last_committed_url.IsAboutBlank() ||
+      // After clicking a link in various apps (eg gchat), a blank redirect
+      // page is left behind. Remove it to clean up.
+      // WasInitiatedByLinkClick()
+      // returns false for links clicked from apps.
+      !handle->WasInitiatedByLinkClick()) {
+    browser_keep_alive = std::make_unique<ScopedKeepAlive>(
+        KeepAliveOrigin::APP_LAUNCH, KeepAliveRestartOption::ENABLED);
+    if (!profile->IsOffTheRecord()) {
+      profile_keep_alive = std::make_unique<ScopedProfileKeepAlive>(
+          profile, ProfileKeepAliveOrigin::kAppWindow);
+    }
+    web_contents->ClosePage();
+  }
+
+  base::OnceClosure launch_callback = base::BindOnce(
+      [](std::unique_ptr<ScopedKeepAlive> browser_keep_alive,
+         std::unique_ptr<ScopedProfileKeepAlive> profile_keep_alive) {
+        // Note: This callback currently serves to own the "keep alive" objects
+        // until the launch is complete.
+        if (GetLinkCaptureLaunchCallbackForTesting()) {               // IN-TEST
+          std::move(GetLinkCaptureLaunchCallbackForTesting()).Run();  // IN-TEST
+        }
+      },
+      std::move(browser_keep_alive), std::move(profile_keep_alive));
+
+  // The tab may have been closed, which runs async and causes the browser
+  // window to be refocused. Post a task to launch the app to ensure launching
+  // happens after the tab closed, otherwise the opened app window might be
+  // inactivated.
+  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+      FROM_HERE, base::BindOnce(std::move(launch_link_capture.value()),
+                                std::move(launch_callback)));
+  return content::NavigationThrottle::CANCEL_AND_IGNORE;
+}
+
+LinkCapturingNavigationThrottle::LinkCapturingNavigationThrottle(
+    content::NavigationHandle* navigation_handle,
+    std::unique_ptr<Delegate> delegate)
+    : content::NavigationThrottle(navigation_handle),
+      delegate_(std::move(delegate)) {}
+
+}  // namespace apps
diff --git a/chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.h b/chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.h
new file mode 100644
index 0000000..ed86ac9
--- /dev/null
+++ b/chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.h
@@ -0,0 +1,96 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_APPS_LINK_CAPTURING_LINK_CAPTURING_NAVIGATION_THROTTLE_H_
+#define CHROME_BROWSER_APPS_LINK_CAPTURING_LINK_CAPTURING_NAVIGATION_THROTTLE_H_
+
+#include <memory>
+
+#include "content/public/browser/navigation_throttle.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
+#include "ui/base/page_transition_types.h"
+#include "url/gurl.h"
+
+class Profile;
+
+namespace content {
+class NavigationHandle;
+class WebContents;
+}  // namespace content
+
+namespace apps {
+
+// Allows canceling a navigation to instead be routed to an installed app.
+class LinkCapturingNavigationThrottle : public content::NavigationThrottle {
+ public:
+  using ThrottleCheckResult = content::NavigationThrottle::ThrottleCheckResult;
+
+  static bool IsCapturableLinkNavigation(ui::PageTransition page_transition,
+                                         bool allow_form_submit,
+                                         bool is_in_fenced_frame_tree,
+                                         bool has_user_gesture);
+
+  // Removes |mask| bits from |page_transition|.
+  static ui::PageTransition MaskOutPageTransition(
+      ui::PageTransition page_transition,
+      ui::PageTransition mask);
+
+  using LaunchCallback =
+      base::OnceCallback<void(base::OnceClosure on_launch_complete)>;
+
+  class Delegate {
+   public:
+    virtual ~Delegate();
+
+    virtual bool ShouldCancelThrottleCreation(
+        content::NavigationHandle* handle) = 0;
+
+    // If the return value is a nullopt, then no capture was possible.
+    // Otherwise, the returned closure will launch the application at the
+    // appropriate URL.
+    virtual absl::optional<LaunchCallback> CreateLinkCaptureLaunchClosure(
+        Profile* profile,
+        content::WebContents* web_contents,
+        const GURL& url,
+        bool is_navigation_from_link) = 0;
+  };
+
+  // Possibly creates a navigation throttle that checks if any installed apps
+  // can handle the URL being navigated to.
+  static std::unique_ptr<content::NavigationThrottle> MaybeCreate(
+      content::NavigationHandle* handle,
+      std::unique_ptr<Delegate> delegate);
+
+  static base::OnceClosure& GetLinkCaptureLaunchCallbackForTesting();
+
+  LinkCapturingNavigationThrottle(const LinkCapturingNavigationThrottle&) =
+      delete;
+  LinkCapturingNavigationThrottle& operator=(
+      const LinkCapturingNavigationThrottle&) = delete;
+  ~LinkCapturingNavigationThrottle() override;
+
+  // content::NavigationHandle overrides
+  const char* GetNameForLogging() override;
+  ThrottleCheckResult WillStartRequest() override;
+  ThrottleCheckResult WillRedirectRequest() override;
+
+  // Visible for testing.
+  static bool IsGoogleRedirectorUrl(const GURL& url);
+  // Visible for testing.
+  static bool ShouldOverrideUrlIfRedirected(const GURL& previous_url,
+                                            const GURL& current_url);
+
+ private:
+  explicit LinkCapturingNavigationThrottle(
+      content::NavigationHandle* navigation_handle,
+      std::unique_ptr<Delegate> delegate);
+  std::unique_ptr<Delegate> delegate_;
+  GURL starting_url_;
+
+  ThrottleCheckResult HandleRequest();
+};
+
+}  // namespace apps
+
+#endif  // CHROME_BROWSER_APPS_LINK_CAPTURING_LINK_CAPTURING_NAVIGATION_THROTTLE_H_
diff --git a/chrome/browser/apps/link_capturing/link_capturing_navigation_throttle_unittest.cc b/chrome/browser/apps/link_capturing/link_capturing_navigation_throttle_unittest.cc
new file mode 100644
index 0000000..ae6abe6
--- /dev/null
+++ b/chrome/browser/apps/link_capturing/link_capturing_navigation_throttle_unittest.cc
@@ -0,0 +1,270 @@
+// Copyright 2023 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/apps/link_capturing/link_capturing_navigation_throttle.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/base/page_transition_types.h"
+#include "url/gurl.h"
+
+namespace apps {
+namespace {
+
+TEST(LinkCapturingNavigationThrottleTest, TestIsGoogleRedirectorUrl) {
+  // Test that redirect urls with different TLDs are still recognized.
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsGoogleRedirectorUrl(
+      GURL("https://www.google.com.au/url?q=whatever")));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsGoogleRedirectorUrl(
+      GURL("https://www.google.com.mx/url?q=hotpot")));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsGoogleRedirectorUrl(
+      GURL("https://www.google.co/url?q=query")));
+
+  // Non-google domains shouldn't be used as valid redirect links.
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsGoogleRedirectorUrl(
+      GURL("https://www.not-google.com/url?q=query")));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsGoogleRedirectorUrl(
+      GURL("https://www.gooogle.com/url?q=legit_query")));
+
+  // This method only takes "/url" as a valid path, it needs to contain a query,
+  // we don't analyze that query as it will expand later on in the same
+  // throttle.
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsGoogleRedirectorUrl(
+      GURL("https://www.google.com/url?q=who_dis")));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsGoogleRedirectorUrl(
+      GURL("http://www.google.com/url?q=who_dis")));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsGoogleRedirectorUrl(
+      GURL("https://www.google.com/url")));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsGoogleRedirectorUrl(
+      GURL("https://www.google.com/link?q=query")));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsGoogleRedirectorUrl(
+      GURL("https://www.google.com/link")));
+}
+
+TEST(LinkCapturingNavigationThrottleTest, TestShouldOverrideUrlLoading) {
+  // A navigation from chrome-extension scheme cannot be overridden.
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("chrome-extension://fake_document"), GURL("http://www.a.com")));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("chrome-extension://fake_document"), GURL("https://www.a.com")));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("chrome-extension://fake_a"), GURL("chrome-extension://fake_b")));
+
+  // Other navigations can be overridden.
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("http://www.google.com"), GURL("http://www.not-google.com/")));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("http://www.not-google.com"), GURL("http://www.google.com/")));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("http://www.google.com"), GURL("http://www.google.com/")));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("http://a.google.com"), GURL("http://b.google.com/")));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("http://a.not-google.com"), GURL("http://b.not-google.com")));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("chrome://fake_document"), GURL("http://www.a.com")));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("file://fake_document"), GURL("http://www.a.com")));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("chrome://fake_document"), GURL("https://www.a.com")));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("file://fake_document"), GURL("https://www.a.com")));
+
+  // A navigation going to a redirect url cannot be overridden, unless there's
+  // no query or the path is not valid.
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("http://www.google.com"), GURL("https://www.google.com/url?q=b")));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("https://www.a.com"), GURL("https://www.google.com/url?q=a")));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("https://www.a.com"), GURL("https://www.google.com/url")));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::ShouldOverrideUrlIfRedirected(
+      GURL("https://www.a.com"), GURL("https://www.google.com/link?q=a")));
+}
+
+// Tests that LinkCapturingNavigationThrottle::ShouldIgnoreNavigation returns
+// false only for PAGE_TRANSITION_LINK when |allow_form_submit| is false and
+// |is_in_fenced_frame_tree| is false.
+TEST(LinkCapturingNavigationThrottleTest,
+     TestShouldIgnoreNavigationWithCoreTypes) {
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PAGE_TRANSITION_LINK, false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PAGE_TRANSITION_TYPED, false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PAGE_TRANSITION_AUTO_BOOKMARK, false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PAGE_TRANSITION_AUTO_SUBFRAME, false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PAGE_TRANSITION_MANUAL_SUBFRAME, false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PAGE_TRANSITION_GENERATED, false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PAGE_TRANSITION_AUTO_TOPLEVEL, false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PAGE_TRANSITION_FORM_SUBMIT, false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PAGE_TRANSITION_RELOAD, false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PAGE_TRANSITION_KEYWORD, false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PAGE_TRANSITION_KEYWORD_GENERATED, false, false, false));
+
+  static_assert(static_cast<int32_t>(ui::PAGE_TRANSITION_KEYWORD_GENERATED) ==
+                    static_cast<int32_t>(ui::PAGE_TRANSITION_LAST_CORE),
+                "Not all core transition types are covered here");
+}
+
+// Test that LinkCapturingNavigationThrottle::ShouldIgnoreNavigation accepts
+// FORM_SUBMIT when |allow_form_submit| is true.
+TEST(LinkCapturingNavigationThrottleTest, TestFormSubmit) {
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PAGE_TRANSITION_FORM_SUBMIT, true, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PAGE_TRANSITION_FORM_SUBMIT, false, false, false));
+}
+
+// Tests that LinkCapturingNavigationThrottle::ShouldIgnoreNavigation returns
+// true when no qualifiers except client redirect and server redirect are
+// provided when |is_in_fenced_frame_tree| is false.
+TEST(LinkCapturingNavigationThrottleTest,
+     TestShouldIgnoreNavigationWithLinkWithQualifiers) {
+  // The navigation is triggered by Forward or Back button.
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
+                                ui::PAGE_TRANSITION_FORWARD_BACK),
+      false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_FORM_SUBMIT |
+                                ui::PAGE_TRANSITION_FORWARD_BACK),
+      false, false, false));
+  // The user used the address bar to trigger the navigation.
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
+                                ui::PAGE_TRANSITION_FROM_ADDRESS_BAR),
+      false, false, false));
+  // The user pressed the Home button.
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
+                                ui::PAGE_TRANSITION_HOME_PAGE),
+      false, false, false));
+  // ARC (for example) opened the link in Chrome.
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
+                                ui::PAGE_TRANSITION_FROM_API),
+      false, false, false));
+  // The navigation is triggered by a client side redirect.
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
+                                ui::PAGE_TRANSITION_CLIENT_REDIRECT),
+      false, false, false));
+  // Also tests the case with 2+ qualifiers.
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
+                                ui::PAGE_TRANSITION_FROM_ADDRESS_BAR |
+                                ui::PAGE_TRANSITION_CLIENT_REDIRECT),
+      false, false, false));
+  // The navigation is triggered by a server side redirect.
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
+                                ui::PAGE_TRANSITION_SERVER_REDIRECT),
+      false, false, false));
+  // Also tests the case with 2+ qualifiers.
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
+                                ui::PAGE_TRANSITION_FROM_ADDRESS_BAR |
+                                ui::PAGE_TRANSITION_SERVER_REDIRECT),
+      false, false, false));
+}
+
+// Just in case, does the same with ui::PAGE_TRANSITION_TYPED.
+TEST(LinkCapturingNavigationThrottleTest,
+     TestShouldIgnoreNavigationWithTypedWithQualifiers) {
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
+                                ui::PAGE_TRANSITION_FORWARD_BACK),
+      false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
+                                ui::PAGE_TRANSITION_FORWARD_BACK),
+      false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
+                                ui::PAGE_TRANSITION_FROM_ADDRESS_BAR),
+      false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
+                                ui::PAGE_TRANSITION_HOME_PAGE),
+      false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
+                                ui::PAGE_TRANSITION_FROM_API),
+      false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
+                                ui::PAGE_TRANSITION_FROM_ADDRESS_BAR |
+                                ui::PAGE_TRANSITION_CLIENT_REDIRECT),
+      false, false, false));
+}
+
+// Test that LinkCapturingNavigationThrottle::ShouldIgnoreNavigation accepts
+// SERVER_REDIRECT and CLIENT_REDIRECT when |is_in_fenced_frame_tree| is false.
+TEST(LinkCapturingNavigationThrottleTest,
+     TestShouldIgnoreNavigationWithClientRedirect) {
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK), false, false,
+      false));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
+                                ui::PAGE_TRANSITION_CLIENT_REDIRECT),
+      false, false, false));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
+                                ui::PAGE_TRANSITION_SERVER_REDIRECT),
+      false, false, false));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
+                                ui::PAGE_TRANSITION_SERVER_REDIRECT |
+                                ui::PAGE_TRANSITION_SERVER_REDIRECT),
+      false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
+                                ui::PAGE_TRANSITION_CLIENT_REDIRECT |
+                                ui::PAGE_TRANSITION_HOME_PAGE),
+      false, false, false));
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK |
+                                ui::PAGE_TRANSITION_HOME_PAGE),
+      false, false, false));
+}
+
+// Test that MaskOutPageTransition correctly remove a qualifier from a given
+// |page_transition|.
+TEST(LinkCapturingNavigationThrottleTest, TestMaskOutPageTransition) {
+  ui::PageTransition page_transition = ui::PageTransitionFromInt(
+      ui::PAGE_TRANSITION_LINK | ui::PAGE_TRANSITION_CLIENT_REDIRECT);
+  EXPECT_EQ(
+      static_cast<int>(ui::PAGE_TRANSITION_LINK),
+      static_cast<int>(LinkCapturingNavigationThrottle::MaskOutPageTransition(
+          page_transition, ui::PAGE_TRANSITION_CLIENT_REDIRECT)));
+
+  page_transition = ui::PageTransitionFromInt(
+      ui::PAGE_TRANSITION_LINK | ui::PAGE_TRANSITION_SERVER_REDIRECT);
+  EXPECT_EQ(
+      static_cast<int>(ui::PAGE_TRANSITION_LINK),
+      static_cast<int>(LinkCapturingNavigationThrottle::MaskOutPageTransition(
+          page_transition, ui::PAGE_TRANSITION_SERVER_REDIRECT)));
+}
+
+// Test that LinkCapturingNavigationThrottle::ShouldIgnoreNavigation accepts iff
+// |has_user_gesture| is true when |is_in_fenced_frame_tree| is true.
+TEST(LinkCapturingNavigationThrottleTest, TestInFencedFrameTree) {
+  EXPECT_FALSE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_AUTO_SUBFRAME), false, true,
+      false));
+  EXPECT_TRUE(LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+      ui::PageTransitionFromInt(ui::PAGE_TRANSITION_AUTO_SUBFRAME), false, true,
+      true));
+}
+
+}  // namespace
+}  // namespace apps
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index ec9545d..72839015 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -553,12 +553,11 @@
         // BUILDFLAG(IS_CHROMEOS_ASH)
 
 #if !BUILDFLAG(IS_ANDROID)
+#include "chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.h"
 #if BUILDFLAG(IS_CHROMEOS)
-#include "chrome/browser/apps/intent_helper/chromeos_apps_navigation_throttle.h"
 #include "chrome/browser/apps/intent_helper/chromeos_disabled_apps_throttle.h"
+#include "chrome/browser/apps/link_capturing/chromeos_link_capturing_delegate.h"
 #include "chrome/browser/policy/system_features_disable_list_policy_handler.h"
-#else
-#include "chrome/browser/apps/intent_helper/apps_navigation_throttle.h"
 #endif
 #endif
 
@@ -5048,25 +5047,19 @@
   }
 #endif
 
-#if !BUILDFLAG(IS_ANDROID)
 #if BUILDFLAG(IS_CHROMEOS)
   auto disabled_app_throttle =
       apps::ChromeOsDisabledAppsThrottle::MaybeCreate(handle);
   if (disabled_app_throttle) {
     throttles.push_back(std::move(disabled_app_throttle));
   }
-#endif  // BUILDFLAG(IS_CHROMEOS)
-
   auto url_to_apps_throttle =
-#if BUILDFLAG(IS_CHROMEOS)
-      apps::ChromeOsAppsNavigationThrottle::MaybeCreate(handle);
-#else
-      apps::AppsNavigationThrottle::MaybeCreate(handle);
-#endif  // BUILDFLAG(IS_CHROMEOS)
+      apps::LinkCapturingNavigationThrottle::MaybeCreate(
+          handle, std::make_unique<apps::ChromeOsLinkCapturingDelegate>());
   if (url_to_apps_throttle) {
     throttles.push_back(std::move(url_to_apps_throttle));
   }
-#endif  // !BUILDFLAG(IS_ANDROID)
+#endif  // BUILDFLAG(IS_CHROMEOS)
 
   Profile* profile = Profile::FromBrowserContext(
       handle->GetWebContents()->GetBrowserContext());
diff --git a/chrome/browser/chromeos/BUILD.gn b/chrome/browser/chromeos/BUILD.gn
index f42a90f..a02ce799 100644
--- a/chrome/browser/chromeos/BUILD.gn
+++ b/chrome/browser/chromeos/BUILD.gn
@@ -255,6 +255,7 @@
     "//chrome/browser:browser_process",
     "//chrome/browser:resources",
     "//chrome/browser/apps/app_service",
+    "//chrome/browser/apps/link_capturing",
     "//chrome/browser/browsing_data:constants",
     "//chrome/browser/enterprise/data_controls",
     "//chrome/browser/favicon",
diff --git a/chrome/browser/chromeos/arc/arc_external_protocol_dialog.cc b/chrome/browser/chromeos/arc/arc_external_protocol_dialog.cc
index ecee3c6..2dbfc71 100644
--- a/chrome/browser/chromeos/arc/arc_external_protocol_dialog.cc
+++ b/chrome/browser/chromeos/arc/arc_external_protocol_dialog.cc
@@ -12,7 +12,7 @@
 #include "base/ranges/algorithm.h"
 #include "build/chromeos_buildflags.h"
 #include "chrome/app/vector_icons/vector_icons.h"
-#include "chrome/browser/apps/intent_helper/page_transition_util.h"
+#include "chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.h"
 #include "chrome/browser/chromeos/arc/arc_web_contents_data.h"
 #include "chrome/browser/sharing/click_to_call/click_to_call_metrics.h"
 #include "chrome/browser/sharing/click_to_call/click_to_call_ui_controller.h"
@@ -91,8 +91,9 @@
   }
 
   // Append the previous list by moving its elements.
-  for (auto& entry : picker_entries)
+  for (auto& entry : picker_entries) {
     all_entries.emplace_back(std::move(entry));
+  }
 
   return all_entries;
 }
@@ -109,8 +110,9 @@
     IntentPickerResponseWithDevices callback) {
   Browser* browser =
       web_contents ? chrome::FindBrowserWithWebContents(web_contents) : nullptr;
-  if (!browser)
+  if (!browser) {
     return false;
+  }
 
   bool has_apps = !app_info.empty();
   bool has_devices = false;
@@ -125,12 +127,14 @@
         ClickToCallUiController::GetOrCreateFromWebContents(web_contents);
     devices = controller->GetDevices();
     has_devices = !devices.empty();
-    if (has_devices)
+    if (has_devices) {
       app_info = AddDevices(devices, std::move(app_info));
+    }
   }
 
-  if (app_info.empty())
+  if (app_info.empty()) {
     return false;
+  }
 
   IntentPickerTabHelper::ShowOrHideIcon(
       web_contents,
@@ -140,16 +144,18 @@
       initiating_origin,
       base::BindOnce(std::move(callback), std::move(devices), bubble_type));
 
-  if (controller)
+  if (controller) {
     controller->OnIntentPickerShown(has_devices, has_apps);
+  }
 
   return true;
 }
 
 void CloseTabIfNeeded(base::WeakPtr<WebContents> web_contents,
                       bool safe_to_bypass_ui) {
-  if (!web_contents)
+  if (!web_contents) {
     return;
+  }
 
   if (web_contents->GetController().IsInitialNavigation() ||
       safe_to_bypass_ui) {
@@ -162,8 +168,9 @@
     const std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo>&
         handlers) {
   for (const auto& handler : handlers) {
-    if (handler.package_name == kArcIntentHelperPackageName)
+    if (handler.package_name == kArcIntentHelperPackageName) {
       return true;
+    }
   }
   return false;
 }
@@ -185,8 +192,9 @@
 
 // Shows |url| in the current tab.
 void OpenUrlInChrome(base::WeakPtr<WebContents> web_contents, const GURL& url) {
-  if (!web_contents)
+  if (!web_contents) {
     return;
+  }
 
   const ui::PageTransition page_transition_type =
       ui::PageTransitionFromInt(ui::PAGE_TRANSITION_LINK);
@@ -246,8 +254,9 @@
       // Since |package_name| is "Chrome", and |fallback_url| is not null, the
       // URL must be either http or https. Check it just in case, and if not,
       // fallback to HANDLE_URL_IN_ARC;
-      if (out_url_and_activity_name->first.SchemeIsHTTPOrHTTPS())
+      if (out_url_and_activity_name->first.SchemeIsHTTPOrHTTPS()) {
         return GetActionResult::OPEN_URL_IN_CHROME;
+      }
 
       LOG(WARNING) << "Failed to handle " << out_url_and_activity_name->first
                    << " in Chrome. Falling back to ARC...";
@@ -319,8 +328,9 @@
     for (size_t i = 0; i < handlers.size(); ++i) {
       const ArcIntentHelperMojoDelegate::IntentHandlerInfo& handler =
           handlers[i];
-      if (!handler.is_preferred)
+      if (!handler.is_preferred) {
         continue;
+      }
       // This is another way to bypass the UI, since the user already expressed
       // some sort of preference.
       *in_out_safe_to_bypass_ui = true;
@@ -352,8 +362,9 @@
       arc::ArcWebContentsData::ArcWebContentsData::kArcTransitionFlag;
   arc::ArcWebContentsData* arc_data =
       static_cast<arc::ArcWebContentsData*>(web_contents->GetUserData(key));
-  if (!arc_data)
+  if (!arc_data) {
     return false;
+  }
 
   web_contents->RemoveUserData(key);
   return true;
@@ -364,8 +375,9 @@
     const std::vector<std::unique_ptr<syncer::DeviceInfo>>& devices,
     const std::string& device_guid,
     const GURL& url) {
-  if (!web_contents)
+  if (!web_contents) {
     return;
+  }
 
   const auto it =
       base::ranges::find(devices, device_guid, &syncer::DeviceInfo::guid);
@@ -394,8 +406,9 @@
   const GetActionResult result =
       GetAction(url, handlers, selected_app_index, &url_and_activity_name,
                 &safe_to_bypass_ui);
-  if (out_result)
+  if (out_result) {
     *out_result = result;
+  }
 
   switch (result) {
     case GetActionResult::OPEN_URL_IN_CHROME:
@@ -440,10 +453,11 @@
     const std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo>&
         handlers) {
   const GURL url_to_open_in_chrome = GetUrlToNavigateOnDeactivate(handlers);
-  if (url_to_open_in_chrome.is_empty())
+  if (url_to_open_in_chrome.is_empty()) {
     CloseTabIfNeeded(web_contents, safe_to_bypass_ui);
-  else
+  } else {
     OpenUrlInChrome(web_contents, url_to_open_in_chrome);
+  }
 }
 
 size_t GetAppIndex(
@@ -451,8 +465,9 @@
         app_candidates,
     const std::string& selected_app_package) {
   for (size_t i = 0; i < app_candidates.size(); ++i) {
-    if (app_candidates[i].package_name == selected_app_package)
+    if (app_candidates[i].package_name == selected_app_package) {
       return i;
+    }
   }
   return app_candidates.size();
 }
@@ -512,8 +527,9 @@
   const size_t selected_app_index = GetAppIndex(handlers, selected_app_package);
 
   // Make sure ARC intent helper instance is connected.
-  if (!mojo_delegate->IsArcAvailable())
+  if (!mojo_delegate->IsArcAvailable()) {
     reason = apps::IntentPickerCloseReason::ERROR_AFTER_PICKER;
+  }
 
   if (reason == apps::IntentPickerCloseReason::OPEN_APP ||
       reason == apps::IntentPickerCloseReason::STAY_IN_CHROME) {
@@ -602,8 +618,9 @@
       web_contents ? chrome::FindBrowserWithWebContents(web_contents.get())
                    : nullptr;
 
-  if (!browser)
+  if (!browser) {
     return std::move(handled_cb).Run(false);
+  }
 
   bool handled = MaybeAddDevicesAndShowPicker(
       url, initiating_origin, web_contents.get(), std::move(app_info),
@@ -715,12 +732,14 @@
   DCHECK(!url.SchemeIsHTTPOrHTTPS()) << url;
 
   // For external protocol navigation, always ignore the FROM_API qualifier.
-  const ui::PageTransition masked_page_transition = apps::MaskOutPageTransition(
-      page_transition, ui::PAGE_TRANSITION_FROM_API);
+  const ui::PageTransition masked_page_transition =
+      apps::LinkCapturingNavigationThrottle::MaskOutPageTransition(
+          page_transition, ui::PAGE_TRANSITION_FROM_API);
 
-  if (apps::ShouldIgnoreNavigation(masked_page_transition,
-                                   /*allow_form_submit=*/true,
-                                   is_in_fenced_frame_tree, has_user_gesture)) {
+  if (!apps::LinkCapturingNavigationThrottle::IsCapturableLinkNavigation(
+          masked_page_transition,
+          /*allow_form_submit=*/true, is_in_fenced_frame_tree,
+          has_user_gesture)) {
     LOG(WARNING) << "RunArcExternalProtocolDialog: ignoring " << url
                  << " with PageTransition=" << masked_page_transition
                  << ", is_in_fenced_frame_tree=" << is_in_fenced_frame_tree
diff --git a/chrome/browser/ui/ash/projector/projector_navigation_throttle_browsertest.cc b/chrome/browser/ui/ash/projector/projector_navigation_throttle_browsertest.cc
index 673ddf8..a98a932 100644
--- a/chrome/browser/ui/ash/projector/projector_navigation_throttle_browsertest.cc
+++ b/chrome/browser/ui/ash/projector/projector_navigation_throttle_browsertest.cc
@@ -13,7 +13,7 @@
 #include "base/test/test_mock_time_task_runner.h"
 #include "base/time/time.h"
 #include "chrome/browser/apps/app_service/metrics/app_service_metrics.h"
-#include "chrome/browser/apps/intent_helper/chromeos_apps_navigation_throttle.h"
+#include "chrome/browser/apps/link_capturing/chromeos_link_capturing_delegate.h"
 #include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
@@ -69,7 +69,7 @@
     base::TimeDelta forward_by = start_time - task_runner_->Now();
     EXPECT_LT(base::TimeDelta(), forward_by);
     task_runner_->AdvanceMockTickClock(forward_by);
-    apps::ChromeOsAppsNavigationThrottle::SetClockForTesting(
+    apps::ChromeOsLinkCapturingDelegate::SetClockForTesting(
         task_runner_->GetMockTickClock());
   }
 
diff --git a/chrome/browser/ui/web_applications/BUILD.gn b/chrome/browser/ui/web_applications/BUILD.gn
index d272553..f784c4e 100644
--- a/chrome/browser/ui/web_applications/BUILD.gn
+++ b/chrome/browser/ui/web_applications/BUILD.gn
@@ -55,7 +55,7 @@
 
   if (!is_chromeos_lacros) {
     sources += [
-      # Test not valid on Lacros as web apps are only enabled in the main
+      # This is not valid on Lacros as web apps are only enabled in the main
       # profile which can never be deleted.
       "web_app_profile_deletion_browsertest.cc",
     ]
@@ -65,6 +65,7 @@
     sources += [
       "app_browser_controller_browsertest_chromeos.cc",
       "web_app_guest_session_browsertest_chromeos.cc",
+      "web_app_link_capturing_browsertest.cc",
     ]
   }
 
@@ -76,6 +77,8 @@
     "//chrome/browser:theme_properties",
     "//chrome/browser/apps/app_service",
     "//chrome/browser/apps/app_service:app_registry_cache_waiter",
+    "//chrome/browser/apps/app_service:test_support",
+    "//chrome/browser/apps/link_capturing",
     "//chrome/browser/browsing_data:constants",
     "//chrome/browser/devtools",
     "//chrome/browser/web_applications:web_applications_test_support",
@@ -115,7 +118,6 @@
   sources = [
     "launch_web_app_browsertest.cc",
     "web_app_badging_browsertest.cc",
-    "web_app_link_capturing_browsertest.cc",
     "web_app_protocol_handling_browsertest.cc",
     "web_app_tab_restore_browsertest.cc",
     "web_app_url_handling_browsertest.cc",
@@ -133,6 +135,7 @@
     sources += [
       "lacros_web_app_browsertest.cc",
       "lacros_web_app_shelf_browsertest.cc",
+      "web_app_link_capturing_browsertest.cc",
     ]
   }
 
@@ -142,6 +145,7 @@
     "//chrome/app:command_ids",
     "//chrome/browser/apps/app_service",
     "//chrome/browser/apps/app_service:app_registry_cache_waiter",
+    "//chrome/browser/apps/link_capturing",
     "//chrome/browser/browsing_data:constants",
     "//chrome/browser/ui/web_applications/diagnostics:app_service_browser_tests",
     "//chrome/browser/web_applications",
diff --git a/chrome/browser/ui/web_applications/web_app_link_capturing_browsertest.cc b/chrome/browser/ui/web_applications/web_app_link_capturing_browsertest.cc
index 275ca5a0..3ccb0f6 100644
--- a/chrome/browser/ui/web_applications/web_app_link_capturing_browsertest.cc
+++ b/chrome/browser/ui/web_applications/web_app_link_capturing_browsertest.cc
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/location.h"
 #include "base/strings/stringprintf.h"
 #include "base/test/bind.h"
 #include "base/test/test_future.h"
@@ -10,8 +11,8 @@
 #include "chrome/browser/apps/app_service/app_registry_cache_waiter.h"
 #include "chrome/browser/apps/app_service/app_service_proxy.h"
 #include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
-#include "chrome/browser/apps/intent_helper/chromeos_apps_navigation_throttle.h"
 #include "chrome/browser/apps/intent_helper/preferred_apps_test_util.h"
+#include "chrome/browser/apps/link_capturing/link_capturing_navigation_throttle.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_commands.h"
 #include "chrome/browser/ui/browser_list.h"
@@ -26,11 +27,10 @@
 #include "chrome/browser/web_applications/web_app_helpers.h"
 #include "chrome/browser/web_applications/web_app_install_info.h"
 #include "chrome/browser/web_applications/web_app_provider.h"
+#include "chrome/browser/web_applications/web_app_registry_update.h"
 #include "chrome/browser/web_applications/web_app_sync_bridge.h"
 #include "chrome/common/chrome_features.h"
 #include "chrome/test/base/ui_test_utils.h"
-#include "components/embedder_support/switches.h"
-#include "components/page_load_metrics/browser/page_load_metrics_test_waiter.h"
 #include "content/public/common/content_features.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/prerender_test_util.h"
@@ -48,18 +48,23 @@
 using ui_test_utils::BrowserChangeObserver;
 
 namespace web_app {
-
+namespace {
 using ClientMode = LaunchHandler::ClientMode;
 
-#if BUILDFLAG(IS_CHROMEOS)
-
 // Tests that links are captured correctly into an installed WebApp using the
 // 'tabbed' display mode, which allows the webapp window to have multiple tabs.
 class WebAppLinkCapturingBrowserTest : public WebAppNavigationBrowserTest {
  public:
   WebAppLinkCapturingBrowserTest() {
-    feature_list_.InitAndEnableFeature(
-        blink::features::kWebAppEnableLaunchHandler);
+    std::vector<base::test::FeatureRef> features = {
+        blink::features::kWebAppEnableLaunchHandler};
+#if !BUILDFLAG(IS_CHROMEOS)
+    features.push_back(features::kDesktopPWAsLinkCapturing);
+#endif
+    feature_list_.InitWithFeatures(
+        /*enabled_features=*/
+        features,
+        /*disabled_features=*/{});
   }
   ~WebAppLinkCapturingBrowserTest() override = default;
 
@@ -119,7 +124,9 @@
     return observer.Wait();
   }
 
-  void ExpectTabs(Browser* test_browser, std::vector<GURL> urls) {
+  void ExpectTabs(Browser* test_browser,
+                  std::vector<GURL> urls,
+                  base::Location location = FROM_HERE) {
     std::string debug_info = "\nOpen browsers:\n";
     for (Browser* open_browser : *BrowserList::GetInstance()) {
       debug_info += "  ";
@@ -139,6 +146,7 @@
                       "\n";
       }
     }
+    SCOPED_TRACE(location.ToString());
     SCOPED_TRACE(debug_info);
     TabStripModel& tab_strip = *test_browser->tab_strip_model();
     ASSERT_EQ(static_cast<size_t>(tab_strip.count()), urls.size());
@@ -152,7 +160,14 @@
   }
 
   void TurnOnLinkCapturing() {
+#if BUILDFLAG(IS_CHROMEOS)
     apps_util::SetSupportedLinksPreferenceAndWait(profile(), app_id_);
+#else
+    ScopedRegistryUpdate update = provider().sync_bridge_unsafe().BeginUpdate();
+    WebApp* app = update->UpdateApp(app_id_);
+    CHECK(app);
+    app->SetIsUserSelectedAppForSupportedLinks(true);
+#endif  // BUILDFLAG(IS_CHROMEOS)
   }
 
   absl::optional<LaunchHandler> GetLaunchHandler(const AppId& app_id) {
@@ -252,7 +267,7 @@
   // Must wait for link capturing launch to complete so that its keep alives go
   // out of scope.
   base::test::TestFuture<void> future;
-  apps::ChromeOsAppsNavigationThrottle::
+  apps::LinkCapturingNavigationThrottle::
       GetLinkCaptureLaunchCallbackForTesting() = future.GetCallback();
   ASSERT_TRUE(future.Wait());
 }
@@ -283,9 +298,14 @@
     : public WebAppLinkCapturingBrowserTest {
  public:
   WebAppTabStripLinkCapturingBrowserTest() {
-    features_.InitWithFeatures({features::kDesktopPWAsTabStrip,
-                                features::kDesktopPWAsTabStripSettings},
-                               {});
+    std::vector<base::test::FeatureRef> features = {
+        features::kDesktopPWAsTabStrip, features::kDesktopPWAsTabStripSettings};
+#if !BUILDFLAG(IS_CHROMEOS)
+    features.push_back(features::kDesktopPWAsLinkCapturing);
+#endif
+    features_.InitWithFeatures(
+        /*enabled_features=*/features,
+        /*disabled_features=*/{});
   }
 
   void InstallTestTabbedApp() {
@@ -338,6 +358,5 @@
   ExpectTabs(app_browser, {in_scope_1_, in_scope_2_, scope_});
 }
 
-#endif  // BUILDFLAG(IS_CHROMEOS)
-
+}  // namespace
 }  // namespace web_app
diff --git a/chrome/browser/ui/web_applications/web_app_ui_manager_impl.cc b/chrome/browser/ui/web_applications/web_app_ui_manager_impl.cc
index ea53e46..d1ef0c8 100644
--- a/chrome/browser/ui/web_applications/web_app_ui_manager_impl.cc
+++ b/chrome/browser/ui/web_applications/web_app_ui_manager_impl.cc
@@ -158,8 +158,9 @@
   started_ = true;
 
   for (Browser* browser : *BrowserList::GetInstance()) {
-    if (!IsBrowserForInstalledApp(browser))
+    if (!IsBrowserForInstalledApp(browser)) {
       continue;
+    }
 
     ++num_windows_for_apps_map_[GetAppIdForBrowser(browser)];
   }
@@ -184,8 +185,9 @@
   DCHECK(started_);
 
   auto it = num_windows_for_apps_map_.find(app_id);
-  if (it == num_windows_for_apps_map_.end())
+  if (it == num_windows_for_apps_map_.end()) {
     return 0;
+  }
 
   return it->second;
 }
@@ -255,19 +257,27 @@
 bool WebAppUiManagerImpl::IsInAppWindow(content::WebContents* web_contents,
                                         const AppId* app_id) const {
   Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
-  if (app_id)
+  if (app_id) {
     return AppBrowserController::IsForWebApp(browser, *app_id);
+  }
   return AppBrowserController::IsWebApp(browser);
 }
 
+bool WebAppUiManagerImpl::IsAppAffiliatedWindowOrNone(
+    content::WebContents* web_contents) const {
+  Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
+  return !browser || browser->is_type_app_popup() || browser->is_type_app();
+}
+
 void WebAppUiManagerImpl::NotifyOnAssociatedAppChanged(
     content::WebContents* web_contents,
     const absl::optional<AppId>& previous_app_id,
     const absl::optional<AppId>& new_app_id) const {
   WebAppMetrics* web_app_metrics = WebAppMetrics::Get(profile_);
   // Unavailable in guest sessions.
-  if (!web_app_metrics)
+  if (!web_app_metrics) {
     return;
+  }
   web_app_metrics->NotifyOnAssociatedAppChanged(web_contents, previous_app_id,
                                                 new_app_id);
 }
@@ -425,16 +435,18 @@
 
 void WebAppUiManagerImpl::OnBrowserAdded(Browser* browser) {
   DCHECK(started_);
-  if (!IsBrowserForInstalledApp(browser))
+  if (!IsBrowserForInstalledApp(browser)) {
     return;
+  }
 
   ++num_windows_for_apps_map_[GetAppIdForBrowser(browser)];
 }
 
 void WebAppUiManagerImpl::OnBrowserRemoved(Browser* browser) {
   DCHECK(started_);
-  if (!IsBrowserForInstalledApp(browser))
+  if (!IsBrowserForInstalledApp(browser)) {
     return;
+  }
 
   const auto& app_id = GetAppIdForBrowser(browser);
 
@@ -442,15 +454,18 @@
   DCHECK_GT(num_windows_for_app, 0u);
   --num_windows_for_app;
 
-  if (num_windows_for_app > 0)
+  if (num_windows_for_app > 0) {
     return;
+  }
 
   auto it = windows_closed_requests_map_.find(app_id);
-  if (it == windows_closed_requests_map_.end())
+  if (it == windows_closed_requests_map_.end()) {
     return;
+  }
 
-  for (auto& callback : it->second)
+  for (auto& callback : it->second) {
     std::move(callback).Run();
+  }
 
   windows_closed_requests_map_.erase(app_id);
 }
@@ -466,11 +481,13 @@
 #endif  //  BUILDFLAG(IS_WIN)
 
 bool WebAppUiManagerImpl::IsBrowserForInstalledApp(Browser* browser) {
-  if (browser->profile() != profile_)
+  if (browser->profile() != profile_) {
     return false;
+  }
 
-  if (!browser->app_controller())
+  if (!browser->app_controller()) {
     return false;
+  }
 
   return true;
 }
diff --git a/chrome/browser/ui/web_applications/web_app_ui_manager_impl.h b/chrome/browser/ui/web_applications/web_app_ui_manager_impl.h
index e8b8150..2df1bb5 100644
--- a/chrome/browser/ui/web_applications/web_app_ui_manager_impl.h
+++ b/chrome/browser/ui/web_applications/web_app_ui_manager_impl.h
@@ -62,6 +62,8 @@
   bool IsAppInQuickLaunchBar(const AppId& app_id) const override;
   bool IsInAppWindow(content::WebContents* web_contents,
                      const AppId* app_id) const override;
+  bool IsAppAffiliatedWindowOrNone(
+      content::WebContents* web_contents) const override;
   void NotifyOnAssociatedAppChanged(
       content::WebContents* web_contents,
       const absl::optional<AppId>& previous_app_id,
diff --git a/chrome/browser/web_applications/test/fake_web_app_ui_manager.cc b/chrome/browser/web_applications/test/fake_web_app_ui_manager.cc
index 88c605f..363ad57 100644
--- a/chrome/browser/web_applications/test/fake_web_app_ui_manager.cc
+++ b/chrome/browser/web_applications/test/fake_web_app_ui_manager.cc
@@ -96,6 +96,11 @@
   return false;
 }
 
+bool FakeWebAppUiManager::IsAppAffiliatedWindowOrNone(
+    content::WebContents* web_contents) const {
+  return false;
+}
+
 bool FakeWebAppUiManager::CanReparentAppTabToWindow(
     const AppId& app_id,
     bool shortcut_created) const {
diff --git a/chrome/browser/web_applications/test/fake_web_app_ui_manager.h b/chrome/browser/web_applications/test/fake_web_app_ui_manager.h
index 5961582..069b9988 100644
--- a/chrome/browser/web_applications/test/fake_web_app_ui_manager.h
+++ b/chrome/browser/web_applications/test/fake_web_app_ui_manager.h
@@ -48,6 +48,8 @@
   bool IsAppInQuickLaunchBar(const AppId& app_id) const override;
   bool IsInAppWindow(content::WebContents* web_contents,
                      const AppId* app_id) const override;
+  bool IsAppAffiliatedWindowOrNone(
+      content::WebContents* web_contents) const override;
   void NotifyOnAssociatedAppChanged(
       content::WebContents* web_contents,
       const absl::optional<AppId>& previous_app_id,
diff --git a/chrome/browser/web_applications/web_app_ui_manager.h b/chrome/browser/web_applications/web_app_ui_manager.h
index d4b8961..b6a4e90 100644
--- a/chrome/browser/web_applications/web_app_ui_manager.h
+++ b/chrome/browser/web_applications/web_app_ui_manager.h
@@ -30,7 +30,7 @@
 namespace content {
 class WebContents;
 class NavigationHandle;
-}
+}  // namespace content
 
 namespace web_app {
 
@@ -130,6 +130,11 @@
   // |app_id|, or any web app window if |app_id| is nullptr.
   virtual bool IsInAppWindow(content::WebContents* web_contents,
                              const AppId* app_id = nullptr) const = 0;
+  // Returns true if the given web contents is associated with an app window, or
+  // if there is no browser associated with this web contents yet.
+  // TODO(https://crbug.com/1474984): Remove the 'none' condition here.
+  virtual bool IsAppAffiliatedWindowOrNone(
+      content::WebContents* web_contents) const = 0;
   virtual void NotifyOnAssociatedAppChanged(
       content::WebContents* web_contents,
       const absl::optional<AppId>& previous_app_id,
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 3fc26a48..2ce5a3d3 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -4661,6 +4661,7 @@
         "//ash/webui/shortcut_customization_ui/backend/search:mojo_bindings",
         "//ash/webui/web_applications/test:test_support",
         "//chrome/browser/apps/app_preload_service:browser_tests",
+        "//chrome/browser/apps/link_capturing:link_capturing",
         "//chrome/browser/ash",
         "//chrome/browser/ash:add_remove_user_event_proto",
         "//chrome/browser/ash:arc_test_support",
@@ -7221,8 +7222,6 @@
     # !is_android
     sources += [
       "../browser/apps/intent_helper/intent_picker_auto_display_prefs_unittest.cc",
-      "../browser/apps/intent_helper/intent_picker_internal_unittest.cc",
-      "../browser/apps/intent_helper/page_transition_util_unittest.cc",
       "../browser/autocomplete/tab_matcher_desktop_unittest.cc",
       "../browser/autofill/autofill_image_fetcher_impl_unittest.cc",
       "../browser/browser_commands_unittest.cc",
@@ -7806,6 +7805,7 @@
       "//chrome/browser/apps/app_service:app_registry_cache_waiter",
       "//chrome/browser/apps/app_service:test_support",
       "//chrome/browser/apps/app_service:unit_tests",
+      "//chrome/browser/apps/link_capturing:unit_tests",
       "//chrome/browser/companion/core",
       "//chrome/browser/companion/core/mojom:mojo_bindings",
       "//chrome/browser/companion/core/proto",