diff --git a/android_webview/browser/aw_web_contents_delegate.cc b/android_webview/browser/aw_web_contents_delegate.cc
index 28a4c1049d86c..23d2d07a8f1f2 100644
--- a/android_webview/browser/aw_web_contents_delegate.cc
+++ b/android_webview/browser/aw_web_contents_delegate.cc
@@ -296,7 +296,7 @@ void AwWebContentsDelegate::UpdateUserGestureCarryoverInfo(
   auto* intercept_navigation_delegate =
       navigation_interception::InterceptNavigationDelegate::Get(web_contents);
   if (intercept_navigation_delegate)
-    intercept_navigation_delegate->UpdateLastUserGestureCarryoverTimestamp();
+    intercept_navigation_delegate->OnResourceRequestWithGesture();
 }
 
 scoped_refptr<content::FileSelectListener>
diff --git a/android_webview/java/src/org/chromium/android_webview/AwContents.java b/android_webview/java/src/org/chromium/android_webview/AwContents.java
index 20f70e752432a..72263bf55d089 100644
--- a/android_webview/java/src/org/chromium/android_webview/AwContents.java
+++ b/android_webview/java/src/org/chromium/android_webview/AwContents.java
@@ -689,8 +689,7 @@ public class AwContents implements SmartClipProvider {
     //
     private class InterceptNavigationDelegateImpl extends InterceptNavigationDelegate {
         @Override
-        public boolean shouldIgnoreNavigation(NavigationHandle navigationHandle, GURL escapedUrl,
-                boolean applyUserGestureCarryover) {
+        public boolean shouldIgnoreNavigation(NavigationHandle navigationHandle, GURL escapedUrl) {
             // The shouldOverrideUrlLoading call might have resulted in posting messages to the
             // UI thread. Using sendMessage here (instead of calling onPageStarted directly)
             // will allow those to run in order.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/compositor/bottombar/OverlayPanelContent.java b/chrome/android/java/src/org/chromium/chrome/browser/compositor/bottombar/OverlayPanelContent.java
index 60a39f0c54149..733861a1d2b54 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/compositor/bottombar/OverlayPanelContent.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/compositor/bottombar/OverlayPanelContent.java
@@ -161,8 +161,7 @@ public class OverlayPanelContent {
         }
 
         @Override
-        public boolean shouldIgnoreNavigation(NavigationHandle navigationHandle, GURL escapedUrl,
-                boolean applyUserGestureCarryover) {
+        public boolean shouldIgnoreNavigation(NavigationHandle navigationHandle, GURL escapedUrl) {
             // If either of the required params for the delegate are null, do not call the
             // delegate and ignore the navigation.
             if (mExternalNavHandler == null || navigationHandle == null) return true;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManager.java b/chrome/android/java/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManager.java
index c1e66af00bc8c..9c45de9ba397f 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManager.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/dom_distiller/ReaderModeManager.java
@@ -228,8 +228,8 @@ public class ReaderModeManager extends EmptyTabObserver implements UserData {
 
         mCustomTabNavigationDelegate = new InterceptNavigationDelegate() {
             @Override
-            public boolean shouldIgnoreNavigation(NavigationHandle navigationHandle,
-                    GURL escapedUrl, boolean applyUserGestureCarryover) {
+            public boolean shouldIgnoreNavigation(
+                    NavigationHandle navigationHandle, GURL escapedUrl) {
                 if (DomDistillerUrlUtils.isDistilledPage(navigationHandle.getUrl())
                         || navigationHandle.isExternalProtocol()) {
                     return false;
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/externalnav/UrlOverridingTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/externalnav/UrlOverridingTest.java
index ee8996d9a3500..dcc3b5ef1260a 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/externalnav/UrlOverridingTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/externalnav/UrlOverridingTest.java
@@ -17,6 +17,7 @@ import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
+import android.os.SystemClock;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.runner.lifecycle.Stage;
 import android.text.TextUtils;
@@ -35,6 +36,7 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.Mockito;
+import org.mockito.Spy;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 import org.mockito.quality.Strictness;
@@ -133,8 +135,6 @@ public class UrlOverridingTest {
             BASE_PATH + "navigation_from_xhr_callback_parent_frame.html";
     private static final String NAVIGATION_FROM_XHR_CALLBACK_AND_SHORT_TIMEOUT_PAGE =
             BASE_PATH + "navigation_from_xhr_callback_and_short_timeout.html";
-    private static final String NAVIGATION_FROM_XHR_CALLBACK_AND_LONG_TIMEOUT_PAGE =
-            BASE_PATH + "navigation_from_xhr_callback_and_long_timeout.html";
     private static final String NAVIGATION_WITH_FALLBACK_URL_PAGE =
             BASE_PATH + "navigation_with_fallback_url.html";
     private static final String NAVIGATION_WITH_FALLBACK_URL_PARENT_FRAME_PAGE =
@@ -171,6 +171,9 @@ public class UrlOverridingTest {
     @Mock
     private RedirectHandler mRedirectHandler;
 
+    @Spy
+    private RedirectHandler mSpyRedirectHandler;
+
     private static class TestTabObserver extends EmptyTabObserver {
         private final CallbackHelper mFinishCallback;
         private final CallbackHelper mFailCallback;
@@ -579,11 +582,30 @@ public class UrlOverridingTest {
 
     @Test
     @SmallTest
-    public void testNavigationFromXHRCallbackAndLongTimeout() {
+    public void testNavigationFromXHRCallbackAndLongTimeout() throws Exception {
         mActivityTestRule.startMainActivityOnBlankPage();
-        loadUrlAndWaitForIntentUrl(
-                mTestServer.getURL(NAVIGATION_FROM_XHR_CALLBACK_AND_LONG_TIMEOUT_PAGE), true,
+
+        final Tab tab = mActivityTestRule.getActivity().getActivityTab();
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> RedirectHandlerTabHelper.swapHandlerFor(tab, mSpyRedirectHandler));
+
+        // This is a little fragile to code changes, but better than waiting 15 real seconds.
+        Mockito.doReturn(SystemClock.elapsedRealtime()) // Initial Navigation create
+                .doReturn(SystemClock.elapsedRealtime()) // Initial Navigation shouldOverride
+                .doReturn(SystemClock.elapsedRealtime()) // XHR Navigation create
+                .doReturn(SystemClock.elapsedRealtime()
+                        + RedirectHandler.NAVIGATION_CHAIN_TIMEOUT_MILLIS + 1) // xhr callback
+                .when(mSpyRedirectHandler)
+                .currentRealtime();
+
+        @OverrideUrlLoadingResultType
+        int result = loadUrlAndWaitForIntentUrl(
+                mTestServer.getURL(NAVIGATION_FROM_XHR_CALLBACK_AND_SHORT_TIMEOUT_PAGE), true,
                 false);
+
+        Assert.assertEquals(OverrideUrlLoadingResultType.OVERRIDE_WITH_ASYNC_ACTION, result);
+
+        assertMessagePresent();
     }
 
     @Test
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tab/InterceptNavigationDelegateTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tab/InterceptNavigationDelegateTest.java
index 06ce74522a2dd..77cee839b2b6f 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/tab/InterceptNavigationDelegateTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tab/InterceptNavigationDelegateTest.java
@@ -111,11 +111,10 @@ public class InterceptNavigationDelegateTest {
                     new InterceptNavigationDelegateClientImpl(tab);
             InterceptNavigationDelegateImpl delegate = new InterceptNavigationDelegateImpl(client) {
                 @Override
-                public boolean shouldIgnoreNavigation(NavigationHandle navigationHandle,
-                        GURL escapedUrl, boolean applyUserGestureCarryover) {
+                public boolean shouldIgnoreNavigation(
+                        NavigationHandle navigationHandle, GURL escapedUrl) {
                     mNavParamHistory.add(navigationHandle);
-                    return super.shouldIgnoreNavigation(
-                            navigationHandle, escapedUrl, applyUserGestureCarryover);
+                    return super.shouldIgnoreNavigation(navigationHandle, escapedUrl);
                 }
             };
             client.initializeWithDelegate(delegate);
diff --git a/chrome/browser/android/tab_web_contents_delegate_android.cc b/chrome/browser/android/tab_web_contents_delegate_android.cc
index e95c473ee3d16..5ff2d9942fc86 100644
--- a/chrome/browser/android/tab_web_contents_delegate_android.cc
+++ b/chrome/browser/android/tab_web_contents_delegate_android.cc
@@ -439,7 +439,7 @@ void TabWebContentsDelegateAndroid::UpdateUserGestureCarryoverInfo(
   auto* intercept_navigation_delegate =
       navigation_interception::InterceptNavigationDelegate::Get(web_contents);
   if (intercept_navigation_delegate)
-    intercept_navigation_delegate->UpdateLastUserGestureCarryoverTimestamp();
+    intercept_navigation_delegate->OnResourceRequestWithGesture();
 }
 
 content::PictureInPictureResult
diff --git a/chrome/test/data/android/url_overriding/navigation_from_xhr_callback_and_long_timeout.html b/chrome/test/data/android/url_overriding/navigation_from_xhr_callback_and_long_timeout.html
deleted file mode 100644
index d566c7c05d50c..0000000000000
--- a/chrome/test/data/android/url_overriding/navigation_from_xhr_callback_and_long_timeout.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-  <meta name="viewport"
-    content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
-  <script>
-    var xmlhttp = new XMLHttpRequest();
-
-    function openApp() {
-      window.location = 'intent://test/#Intent;scheme=externalappscheme;end';
-    };
-
-    function xhrOnReadyStateChange() {
-      if (xmlhttp.readyState==4 && xmlhttp.status==200) {
-        setTimeout(openApp, 11000);
-      }
-    };
-
-    function xhrAndOpenApp() {
-      xmlhttp.onreadystatechange = xhrOnReadyStateChange;
-      xmlhttp.open("GET", 'hello.html' , true);
-      xmlhttp.send();
-    };
-  </script>
-</head>
-<body style='height:10000px;' onclick='xhrAndOpenApp();'>
-  Click page to open App!!
-</body>
-</html>
diff --git a/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java b/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java
index 72d7fd3f1de9e..33ef01b0f5d1f 100644
--- a/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java
+++ b/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java
@@ -930,6 +930,24 @@ public class ExternalNavigationHandler {
         return false;
     }
 
+    /**
+     * See RedirectHandler#NAVIGATION_CHAIN_TIMEOUT_MILLIS for details. We don't want an unattended
+     * page to redirect to an app.
+     */
+    private boolean isNavigationChainExpired(ExternalNavigationParams params) {
+        if (params.getRedirectHandler() != null
+                && params.getRedirectHandler().isNavigationChainExpired()) {
+            if (DEBUG) {
+                Log.i(TAG,
+                        "Navigation chain expired "
+                                + "(a page waited more than %d seconds to redirect).",
+                        RedirectHandler.NAVIGATION_CHAIN_TIMEOUT_MILLIS);
+            }
+            return true;
+        }
+        return false;
+    }
+
     /**
      * If the intent can't be resolved, we should fall back to the browserFallbackUrl, or try to
      * find the app on the market if no fallback is provided.
@@ -1471,18 +1489,13 @@ public class ExternalNavigationHandler {
         QueryIntentActivitiesSupplier resolvingInfos =
                 new QueryIntentActivitiesSupplier(targetIntent);
 
-        boolean requiresPromptForExternalIntent = false;
-
-        if (redirectShouldStayInApp(params, isExternalProtocol, targetIntent, resolvingInfos)) {
-            requiresPromptForExternalIntent = true;
-        }
-
         boolean intentMatchesNonDefaultWebApk =
                 intentMatchesNonDefaultWebApk(params, resolvingInfos);
-        if (!preferToShowIntentPicker(params, isExternalProtocol, incomingIntentRedirect,
-                    intentMatchesNonDefaultWebApk)) {
-            requiresPromptForExternalIntent = true;
-        }
+
+        boolean requiresPromptForExternalIntent = isNavigationChainExpired(params)
+                || redirectShouldStayInApp(params, isExternalProtocol, targetIntent, resolvingInfos)
+                || !preferToShowIntentPicker(params, isExternalProtocol, incomingIntentRedirect,
+                        intentMatchesNonDefaultWebApk);
 
         // Short-circuit expensive quertyIntentActivities calls below since we won't prompt anyways
         // for protocols the browser can handle.
diff --git a/components/external_intents/android/java/src/org/chromium/components/external_intents/InterceptNavigationDelegateImpl.java b/components/external_intents/android/java/src/org/chromium/components/external_intents/InterceptNavigationDelegateImpl.java
index bd5a8b7e1955c..536d0a0025c17 100644
--- a/components/external_intents/android/java/src/org/chromium/components/external_intents/InterceptNavigationDelegateImpl.java
+++ b/components/external_intents/android/java/src/org/chromium/components/external_intents/InterceptNavigationDelegateImpl.java
@@ -24,6 +24,7 @@ import org.chromium.content_public.browser.NavigationHandle;
 import org.chromium.content_public.browser.UiThreadTaskTraits;
 import org.chromium.content_public.browser.WebContents;
 import org.chromium.content_public.common.ConsoleMessageLevel;
+import org.chromium.ui.base.PageTransition;
 import org.chromium.url.GURL;
 import org.chromium.url.Origin;
 
@@ -103,8 +104,7 @@ public class InterceptNavigationDelegateImpl extends InterceptNavigationDelegate
     }
 
     @Override
-    public boolean shouldIgnoreNavigation(
-            NavigationHandle navigationHandle, GURL escapedUrl, boolean applyUserGestureCarryover) {
+    public boolean shouldIgnoreNavigation(NavigationHandle navigationHandle, GURL escapedUrl) {
         mClient.onNavigationStarted(navigationHandle);
 
         GURL url = escapedUrl;
@@ -135,12 +135,6 @@ public class InterceptNavigationDelegateImpl extends InterceptNavigationDelegate
             return false;
         }
 
-        // Temporarily apply User Gesture Carryover exception for resource requests to the
-        // NavigationHandle.
-        if (applyUserGestureCarryover) {
-            assert !navigationHandle.hasUserGesture();
-            navigationHandle.setUserGestureForCarryover(true);
-        }
         redirectHandler.updateNewUrlLoading(navigationHandle.pageTransition(),
                 navigationHandle.isRedirect(), navigationHandle.hasUserGesture(),
                 lastUserInteractionTime, getLastCommittedEntryIndex(), isInitialNavigation());
@@ -155,10 +149,6 @@ public class InterceptNavigationDelegateImpl extends InterceptNavigationDelegate
 
         mClient.onDecisionReachedForNavigation(navigationHandle, result);
 
-        if (applyUserGestureCarryover) {
-            navigationHandle.setUserGestureForCarryover(false);
-        }
-
         boolean isExternalProtocol = !UrlUtilities.isAcceptedScheme(params.getUrl());
         String protocolType = isExternalProtocol ? "ExternalProtocol" : "InternalProtocol";
         RecordHistogram.recordEnumeratedHistogram(
@@ -186,6 +176,16 @@ public class InterceptNavigationDelegateImpl extends InterceptNavigationDelegate
         }
     }
 
+    @Override
+    public void onResourceRequestWithGesture() {
+        // LINK is the default transition type, and is generally used for everything coming from a
+        // renderer that isn't a form submission (or subframe).
+        @PageTransition
+        int transition = PageTransition.LINK;
+        mClient.getOrCreateRedirectHandler().updateNewUrlLoading(transition, false, true,
+                mClient.getLastUserInteractionTime(), getLastCommittedEntryIndex(), false);
+    }
+
     /**
      * Returns ExternalNavigationParams.Builder to generate ExternalNavigationParams for
      * ExternalNavigationHandler#shouldOverrideUrlLoading().
diff --git a/components/external_intents/android/java/src/org/chromium/components/external_intents/RedirectHandler.java b/components/external_intents/android/java/src/org/chromium/components/external_intents/RedirectHandler.java
index 5dfcd23c11a39..80b526af75f74 100644
--- a/components/external_intents/android/java/src/org/chromium/components/external_intents/RedirectHandler.java
+++ b/components/external_intents/android/java/src/org/chromium/components/external_intents/RedirectHandler.java
@@ -11,6 +11,8 @@ import android.os.SystemClock;
 import android.provider.Browser;
 import android.text.TextUtils;
 
+import androidx.annotation.VisibleForTesting;
+
 import org.chromium.base.ContextUtils;
 import org.chromium.base.Function;
 import org.chromium.base.IntentUtils;
@@ -39,6 +41,14 @@ public class RedirectHandler {
     private static final int NAVIGATION_TYPE_FROM_RELOAD = 4;
     private static final int NAVIGATION_TYPE_OTHER = 5;
 
+    // Analogous to Transient User Activation in blink (See
+    // https://html.spec.whatwg.org/multipage/interaction.html#tracking-user-activation). We don't
+    // want an "unattended" page to redirect to an app as the user is likely not expecting that.
+    // However, historically there was no timeout like this for external navigation (and instead
+    // touching the screen reset the navigation chain), so this timeout is very generous and should
+    // allow for redirect chains.
+    public static final long NAVIGATION_CHAIN_TIMEOUT_MILLIS = 15000;
+
     private static class IntentState {
         final Intent mInitialIntent;
         final boolean mIsCustomTabIntent;
@@ -57,12 +67,14 @@ public class RedirectHandler {
         }
     }
 
-    private static class NavigationState {
+    private class NavigationState {
         final int mInitialNavigationType;
         final boolean mHasUserStartedNonInitialNavigation;
         boolean mIsOnEffectiveRedirectChain;
         boolean mShouldNotOverrideUrlLoadingOnCurrentRedirectChain;
         boolean mShouldNotBlockOverrideUrlLoadingOnCurrentRedirectionChain;
+        // TODO(https://crbug.com/1286053): Plumb through the user activation time from blink.
+        final long mNavigationChainStartTime = currentRealtime();
 
         NavigationState(int initialNavigationType, boolean hasUserStartedNonInitialNavigation) {
             mInitialNavigationType = initialNavigationType;
@@ -350,6 +362,16 @@ public class RedirectHandler {
         return mIntentState != null ? mIntentState.mInitialIntent : null;
     }
 
+    /**
+     * @return whether the navigation chain has expired, meaning
+     * {@link #NAVIGATION_CHAIN_TIMEOUT_MILLIS} milliseconds passed since a navigation initiated by
+     * the user was started.
+     */
+    public boolean isNavigationChainExpired() {
+        return currentRealtime() - mNavigationState.mNavigationChainStartTime
+                > NAVIGATION_CHAIN_TIMEOUT_MILLIS;
+    }
+
     public void maybeLogExternalRedirectBlockedWithMissingGesture() {
         if (mNavigationState.mInitialNavigationType
                 == NAVIGATION_TYPE_FROM_LINK_WITHOUT_USER_GESTURE) {
@@ -362,4 +384,10 @@ public class RedirectHandler {
                     "Android.Intent.BlockedExternalNavLastGestureTime", millisSinceLastGesture);
         }
     }
+
+    // Facilitates simulated waiting in tests.
+    @VisibleForTesting
+    public long currentRealtime() {
+        return SystemClock.elapsedRealtime();
+    }
 }
diff --git a/components/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.java b/components/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.java
index 327332ca30301..cafdf33ba4c43 100644
--- a/components/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.java
+++ b/components/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.java
@@ -67,6 +67,7 @@ import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.regex.Pattern;
 
 /**
@@ -2543,6 +2544,36 @@ public class ExternalNavigationHandlerTest {
                         START_OTHER_ACTIVITY);
     }
 
+    @Test
+    @SmallTest
+    public void testExpiredNavigationChain() {
+        mDelegate.add(new IntentActivity(YOUTUBE_MOBILE_URL, YOUTUBE_PACKAGE_NAME));
+
+        AtomicBoolean isExpired = new AtomicBoolean(false);
+        RedirectHandler redirectHandler = new RedirectHandler() {
+            @Override
+            public boolean isNavigationChainExpired() {
+                return isExpired.get();
+            }
+        };
+
+        // User clicks a link.
+        redirectHandler.updateNewUrlLoading(PageTransition.LINK, false, true, 0, 0, false);
+
+        // Redirects to youtube with javascript simulated link click.
+        redirectHandler.updateNewUrlLoading(PageTransition.LINK, false, false, 0, 1, false);
+        checkUrl(YOUTUBE_MOBILE_URL)
+                .withRedirectHandler(redirectHandler)
+                .expecting(OverrideUrlLoadingResultType.OVERRIDE_WITH_EXTERNAL_INTENT,
+                        START_OTHER_ACTIVITY);
+
+        // Page takes > 15 seconds to redirect.
+        isExpired.set(true);
+        checkUrl(YOUTUBE_MOBILE_URL)
+                .withRedirectHandler(redirectHandler)
+                .expecting(OverrideUrlLoadingResultType.NO_OVERRIDE, IGNORE);
+    }
+
     private static List<ResolveInfo> makeResolveInfos(ResolveInfo... infos) {
         return Arrays.asList(infos);
     }
diff --git a/components/navigation_interception/android/java/src/org/chromium/components/navigation_interception/InterceptNavigationDelegate.java b/components/navigation_interception/android/java/src/org/chromium/components/navigation_interception/InterceptNavigationDelegate.java
index 682095a6b3600..5e24c4c1f06e6 100644
--- a/components/navigation_interception/android/java/src/org/chromium/components/navigation_interception/InterceptNavigationDelegate.java
+++ b/components/navigation_interception/android/java/src/org/chromium/components/navigation_interception/InterceptNavigationDelegate.java
@@ -23,7 +23,7 @@ public abstract class InterceptNavigationDelegate {
      */
     @CalledByNative
     public abstract boolean shouldIgnoreNavigation(
-            NavigationHandle navigationHandle, GURL escapedUrl, boolean applyUserGestureCarryover);
+            NavigationHandle navigationHandle, GURL escapedUrl);
 
     /**
      * This method is called for navigations to external protocols, which on Android are handled in
@@ -50,6 +50,14 @@ public abstract class InterceptNavigationDelegate {
                 true /* isExternalProtocol */,
                 0 /* navigationId - doesn't correspond to a native NavigationHandle*/,
                 false /* isPageActivation */, false /* isReload */);
-        shouldIgnoreNavigation(navigationHandle, escapedUrl, false);
+        shouldIgnoreNavigation(navigationHandle, escapedUrl);
     }
+
+    /**
+     * This method is called when a main frame requests a resource with a user gesture (eg. xhr,
+     * fetch, etc.). The page may wish to redirect to an app after the resource requests completes,
+     * which may be after blink user activation has expired.
+     */
+    @CalledByNative
+    protected void onResourceRequestWithGesture() {}
 }
diff --git a/components/navigation_interception/intercept_navigation_delegate.cc b/components/navigation_interception/intercept_navigation_delegate.cc
index 15c4b5166bc6f..a155dc22dd687 100644
--- a/components/navigation_interception/intercept_navigation_delegate.cc
+++ b/components/navigation_interception/intercept_navigation_delegate.cc
@@ -32,8 +32,6 @@ namespace navigation_interception {
 
 namespace {
 
-const int kMaxValidityOfUserGestureCarryoverInSeconds = 10;
-
 const void* const kInterceptNavigationDelegateUserDataKey =
     &kInterceptNavigationDelegateUserDataKey;
 
@@ -119,16 +117,9 @@ bool InterceptNavigationDelegate::ShouldIgnoreNavigation(
   if (jdelegate.is_null())
     return false;
 
-  bool has_user_gesture = navigation_handle->HasUserGesture();
-  bool apply_user_gesture_carryover =
-      !has_user_gesture &&
-      base::TimeTicks::Now() - last_user_gesture_carryover_timestamp_ <=
-          base::Seconds(kMaxValidityOfUserGestureCarryoverInSeconds);
-
   return Java_InterceptNavigationDelegate_shouldIgnoreNavigation(
       env, jdelegate, navigation_handle->GetJavaNavigationHandle(),
-      url::GURLAndroid::FromNativeGURL(env, escaped_url),
-      apply_user_gesture_carryover);
+      url::GURLAndroid::FromNativeGURL(env, escaped_url));
 }
 
 void InterceptNavigationDelegate::HandleExternalProtocolDialog(
@@ -153,8 +144,12 @@ void InterceptNavigationDelegate::HandleExternalProtocolDialog(
       initiating_origin ? initiating_origin->CreateJavaObject() : nullptr);
 }
 
-void InterceptNavigationDelegate::UpdateLastUserGestureCarryoverTimestamp() {
-  last_user_gesture_carryover_timestamp_ = base::TimeTicks::Now();
+void InterceptNavigationDelegate::OnResourceRequestWithGesture() {
+  JNIEnv* env = base::android::AttachCurrentThread();
+  ScopedJavaLocalRef<jobject> jdelegate = weak_jdelegate_.get(env);
+  if (jdelegate.is_null())
+    return;
+  Java_InterceptNavigationDelegate_onResourceRequestWithGesture(env, jdelegate);
 }
 
 }  // namespace navigation_interception
diff --git a/components/navigation_interception/intercept_navigation_delegate.h b/components/navigation_interception/intercept_navigation_delegate.h
index 6ae9ab57a01d2..b773898f90ee5 100644
--- a/components/navigation_interception/intercept_navigation_delegate.h
+++ b/components/navigation_interception/intercept_navigation_delegate.h
@@ -76,13 +76,12 @@ class InterceptNavigationDelegate : public base::SupportsUserData::Data {
       bool has_user_gesture,
       const absl::optional<url::Origin>& initiating_origin);
 
-  // Updates |last_user_gesture_carryover_timestamp_| when user gesture is
-  // carried over.
-  void UpdateLastUserGestureCarryoverTimestamp();
+  // To be called when a main frame requests a resource with a user gesture (eg.
+  // xrh, fetch, etc.)
+  void OnResourceRequestWithGesture();
 
  private:
   JavaObjectWeakGlobalRef weak_jdelegate_;
-  base::TimeTicks last_user_gesture_carryover_timestamp_;
   bool escape_external_handler_value_ = false;
 };
 
diff --git a/content/public/android/java/src/org/chromium/content_public/browser/NavigationHandle.java b/content/public/android/java/src/org/chromium/content_public/browser/NavigationHandle.java
index b961b68f8069c..fd38b623e145c 100644
--- a/content/public/android/java/src/org/chromium/content_public/browser/NavigationHandle.java
+++ b/content/public/android/java/src/org/chromium/content_public/browser/NavigationHandle.java
@@ -303,14 +303,6 @@ public class NavigationHandle {
         return mIsPageActivation;
     }
 
-    /**
-     * TODO(https://crbug.com/1310013): This is a hack, restoring M99 Chrome behavior for gesture
-     * carryover on resource requests. To be deleted as soon as a better alternative is agreed upon.
-     */
-    public void setUserGestureForCarryover(boolean hasUserGesture) {
-        mHasUserGesture = hasUserGesture;
-    }
-
     /**
      * Whether this navigation was initiated by a page reload.
      */