Add flag to enable External Navigation debug logs

The logs help developers debug why their app links/redirects don't work
when they break, and save Chrome developers lots of time trying to
figure out whether bug reports about broken redirects are the fault of
Chrome or the website.

Bug: 1354611
Change-Id: I7ae9833ef61bf992448d366a66b90bddcd433a6b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3842509
Reviewed-by: Yaron Friedman <[email protected]>
Commit-Queue: Michael Thiessen <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1038851}
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 9c6647c8..c6a6ae2 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -91,6 +91,7 @@
 #include "components/dom_distiller/core/dom_distiller_switches.h"
 #include "components/download/public/common/download_features.h"
 #include "components/error_page/common/error_page_switches.h"
+#include "components/external_intents/android/external_intents_features.h"
 #include "components/feature_engagement/public/feature_constants.h"
 #include "components/feature_engagement/public/feature_list.h"
 #include "components/feed/feed_feature_list.h"
@@ -9148,6 +9149,13 @@
      FEATURE_VALUE_TYPE(features::kDesktopPWAsAppHomePage)},
 #endif  // !BUILDFLAG(IS_CHROMEOS) && !BUILDFLAG(IS_ANDROID)
 
+#if BUILDFLAG(IS_ANDROID)
+    {"external-navigation-debug-logs",
+     flag_descriptions::kExternalNavigationDebugLogsName,
+     flag_descriptions::kExternalNavigationDebugLogsDescription, kOsAndroid,
+     FEATURE_VALUE_TYPE(external_intents::kExternalNavigationDebugLogs)},
+#endif
+
 #if !BUILDFLAG(IS_ANDROID)
     {"battery-saver-mode-available",
      flag_descriptions::kBatterySaverModeAvailableName,
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 0b8edac..0132895 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -3350,6 +3350,13 @@
     "expiry_milestone": -1
   },
   {
+    "name": "external-navigation-debug-logs",
+    "owners": [ "[email protected]", "[email protected]" ],
+    // Used by developers for debugging why Chrome fails to launch their app, by
+    // dumping extra information to logs in official builds.
+    "expiry_milestone": -1
+  },
+  {
     "name": "fast-checkout",
     "owners": [ "vizcay", "bwolfgang", "jkeitel" ],
     "expiry_milestone": 112
diff --git a/chrome/browser/flag-never-expire-list.json b/chrome/browser/flag-never-expire-list.json
index 3d3ed7b..2288718 100644
--- a/chrome/browser/flag-never-expire-list.json
+++ b/chrome/browser/flag-never-expire-list.json
@@ -70,6 +70,7 @@
   "enable-webgpu-developer-features",
   "enable-zero-copy",
   "extensions-on-chrome-urls",
+  "external-navigation-debug-logs",
   "force-color-profile",
   "force-effective-connection-type",
   "force-show-update-menu-badge",
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 8230c1e8..63e04b2 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -3450,6 +3450,12 @@
 const char kExploreSitesDescription[] =
     "Enables portal from new tab page to explore websites.";
 
+const char kExternalNavigationDebugLogsName[] =
+    "External Navigation Debug Logs";
+const char kExternalNavigationDebugLogsDescription[] =
+    "Enables detailed logging to logcat about why Chrome is making decisions "
+    "about whether to allow or block navigation to other apps";
+
 const char kFeatureNotificationGuideName[] = "Feature notification guide";
 const char kFeatureNotificationGuideDescription[] =
     "Enables notifications about chrome features.";
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 5976f44..af7dd832 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1940,6 +1940,9 @@
 extern const char kExploreSitesName[];
 extern const char kExploreSitesDescription[];
 
+extern const char kExternalNavigationDebugLogsName[];
+extern const char kExternalNavigationDebugLogsDescription[];
+
 extern const char kFeatureNotificationGuideName[];
 extern const char kFeatureNotificationGuideDescription[];
 
diff --git a/components/external_intents/android/external_intents_features.cc b/components/external_intents/android/external_intents_features.cc
index 373c5de..4b33881 100644
--- a/components/external_intents/android/external_intents_features.cc
+++ b/components/external_intents/android/external_intents_features.cc
@@ -19,7 +19,7 @@
 // Array of features exposed through the Java ExternalIntentsFeatures API.
 const base::Feature* kFeaturesExposedToJava[] = {
     &kAutofillAssistantGoogleInitiatorOriginCheck,
-    &kScaryExternalNavigationRefactoring};
+    &kExternalNavigationDebugLogs, &kScaryExternalNavigationRefactoring};
 
 }  // namespace
 
@@ -31,6 +31,9 @@
     "AutofillAssistantGoogleInitiatorOriginCheck",
     base::FEATURE_ENABLED_BY_DEFAULT};
 
+const base::Feature kExternalNavigationDebugLogs{
+    "ExternalNavigationDebugLogs", base::FEATURE_DISABLED_BY_DEFAULT};
+
 const base::Feature kScaryExternalNavigationRefactoring{
     "ScaryExternalNavigationRefactoring", base::FEATURE_ENABLED_BY_DEFAULT};
 
diff --git a/components/external_intents/android/external_intents_features.h b/components/external_intents/android/external_intents_features.h
index 4ca6a43..f996120 100644
--- a/components/external_intents/android/external_intents_features.h
+++ b/components/external_intents/android/external_intents_features.h
@@ -11,6 +11,7 @@
 
 // Alphabetical:
 extern const base::Feature kAutofillAssistantGoogleInitiatorOriginCheck;
+extern const base::Feature kExternalNavigationDebugLogs;
 extern const base::Feature kScaryExternalNavigationRefactoring;
 
 }  // namespace external_intents
diff --git a/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsFeatures.java b/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsFeatures.java
index da4bb99..13bf2f1 100644
--- a/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsFeatures.java
+++ b/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsFeatures.java
@@ -22,14 +22,18 @@
 public class ExternalIntentsFeatures extends Features {
     public static final String AUTOFILL_ASSISTANT_GOOGLE_INITIATOR_ORIGIN_CHECK_NAME =
             "AutofillAssistantGoogleInitiatorOriginCheck";
+    public static final String EXTERNAL_NAVIGATION_DEBUG_LOGS_NAME = "ExternalNavigationDebugLogs";
     public static final String SCARY_EXTERNAL_NAVIGATION_REFACTORING_NAME =
             "ScaryExternalNavigationRefactoring";
 
     public static final ExternalIntentsFeatures AUTOFILL_ASSISTANT_GOOGLE_INITIATOR_ORIGIN_CHECK =
             new ExternalIntentsFeatures(0, AUTOFILL_ASSISTANT_GOOGLE_INITIATOR_ORIGIN_CHECK_NAME);
 
+    public static final ExternalIntentsFeatures EXTERNAL_NAVIGATION_DEBUG_LOGS =
+            new ExternalIntentsFeatures(1, EXTERNAL_NAVIGATION_DEBUG_LOGS_NAME);
+
     public static final ExternalIntentsFeatures SCARY_EXTERNAL_NAVIGATION_REFACTORING =
-            new ExternalIntentsFeatures(1, SCARY_EXTERNAL_NAVIGATION_REFACTORING_NAME);
+            new ExternalIntentsFeatures(2, SCARY_EXTERNAL_NAVIGATION_REFACTORING_NAME);
 
     private final int mOrdinal;
 
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 400a30d..a930de8 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
@@ -93,9 +93,6 @@
 public class ExternalNavigationHandler {
     private static final String TAG = "UrlHandler";
 
-    // Enables debug logging on a local build.
-    private static final boolean DEBUG = false;
-
     private static final String WTAI_URL_PREFIX = "wtai://wp/";
     private static final String WTAI_MC_URL_PREFIX = "wtai://wp/mc;";
 
@@ -382,6 +379,10 @@
         mDelegate = delegate;
     }
 
+    private static boolean debug() {
+        return ExternalIntentsFeatures.EXTERNAL_NAVIGATION_DEBUG_LOGS.isEnabled();
+    }
+
     /**
      * Determines whether the URL needs to be sent as an intent to the system,
      * and sends it, if appropriate.
@@ -389,7 +390,7 @@
      *         current tab, or wasn't handled at all.
      */
     public OverrideUrlLoadingResult shouldOverrideUrlLoading(ExternalNavigationParams params) {
-        if (DEBUG) Log.i(TAG, "shouldOverrideUrlLoading called on " + params.getUrl().getSpec());
+        if (debug()) Log.i(TAG, "shouldOverrideUrlLoading called on " + params.getUrl().getSpec());
         Intent targetIntent;
         // Perform generic parsing of the URI to turn it into an Intent.
         if (UrlUtilities.hasIntentScheme(params.getUrl())) {
@@ -437,7 +438,7 @@
             result = handleFallbackUrl(params, targetIntent, browserFallbackUrl,
                     canLaunchExternalFallbackResult.get());
         }
-        if (DEBUG) printDebugShouldOverrideUrlLoadingResultType(result);
+        if (debug()) printDebugShouldOverrideUrlLoadingResultType(result);
         return result;
     }
 
@@ -473,7 +474,7 @@
                         return OverrideUrlLoadingResult.forExternalIntent();
                     }
                 } catch (Exception e) {
-                    if (DEBUG) Log.i(TAG, "Could not parse fallback url as intent");
+                    if (debug()) Log.i(TAG, "Could not parse fallback url as intent");
                 }
             }
 
@@ -493,7 +494,7 @@
         // careful to prevent sandbox escapes.
         // http://crbug.com/364522.
         if (!params.isMainFrame()) {
-            if (DEBUG) Log.i(TAG, "Don't support fallback url in subframes");
+            if (debug()) Log.i(TAG, "Don't support fallback url in subframes");
             return OverrideUrlLoadingResult.forNoOverride();
         }
 
@@ -506,7 +507,7 @@
                             .getAndClearShouldNotBlockOverrideUrlLoadingOnCurrentRedirectionChain()) {
             params.getRedirectHandler().setShouldNotOverrideUrlLoadingOnCurrentRedirectChain();
         }
-        if (DEBUG) Log.i(TAG, "clobberCurrentTab called");
+        if (debug()) Log.i(TAG, "clobberCurrentTab called");
         return clobberCurrentTab(browserFallbackUrl, params.getReferrerUrl(),
                 params.getInitiatorOrigin(), params.isRendererInitiated());
     }
@@ -553,7 +554,7 @@
      */
     private boolean shouldBlockSubframeAppLaunches(ExternalNavigationParams params) {
         if (!params.isMainFrame() && !params.hasUserGesture()) {
-            if (DEBUG) Log.i(TAG, "Subframe navigation without user gesture.");
+            if (debug()) Log.i(TAG, "Subframe navigation without user gesture.");
             return true;
         }
         return false;
@@ -569,7 +570,7 @@
         // navigations started by another app that should still be safe.
         if (incomingIntentRedirect) return false;
         if (params.isApplicationMustBeInForeground() && !mDelegate.isApplicationInForeground()) {
-            if (DEBUG) Log.i(TAG, "App is not in foreground");
+            if (debug()) Log.i(TAG, "App is not in foreground");
             return true;
         }
         return false;
@@ -584,7 +585,7 @@
         if (incomingIntentRedirect) return false;
         if (params.isBackgroundTabNavigation()
                 && !params.areIntentLaunchesAllowedInBackgroundTabs()) {
-            if (DEBUG) Log.i(TAG, "Navigation in background tab");
+            if (debug()) Log.i(TAG, "Navigation in background tab");
             return true;
         }
         return false;
@@ -596,7 +597,7 @@
      */
     private boolean ignoreBackForwardNav(ExternalNavigationParams params) {
         if ((params.getPageTransition() & PageTransition.FORWARD_BACK) != 0) {
-            if (DEBUG) Log.i(TAG, "Forward or back navigation");
+            if (debug()) Log.i(TAG, "Forward or back navigation");
             return true;
         }
         return false;
@@ -606,7 +607,7 @@
     private boolean isInternalPdfDownload(
             boolean isExternalProtocol, ExternalNavigationParams params) {
         if (!isExternalProtocol && isPdfDownload(params.getUrl())) {
-            if (DEBUG) Log.i(TAG, "PDF downloads are now handled internally");
+            if (debug()) Log.i(TAG, "PDF downloads are now handled internally");
             return true;
         }
         return false;
@@ -629,7 +630,7 @@
 
         if (!shouldRequestFileAccess(params.getUrl(), permissionNeeded)) return false;
         requestFilePermissions(params, permissionNeeded);
-        if (DEBUG) Log.i(TAG, "Requesting filesystem access");
+        if (debug()) Log.i(TAG, "Requesting filesystem access");
         return true;
     }
 
@@ -740,7 +741,7 @@
                 mDelegate.isIntentForTrustedCallingApp(targetIntent, resolvingInfos));
         if (shouldStayInApp || handler.shouldNotOverrideUrlLoading()) {
             if (isExternalProtocol) handler.maybeLogExternalRedirectBlockedWithMissingGesture();
-            if (DEBUG) Log.i(TAG, "RedirectHandler decision");
+            if (debug()) Log.i(TAG, "RedirectHandler decision");
             return true;
         }
         return false;
@@ -757,7 +758,7 @@
         if (params.isFromIntent() && mDelegate.shouldLaunchWebApksOnInitialIntent()) {
             String packageName = pickWebApkIfSoleIntentHandler(params, resolvingInfos);
             if (packageName != null) {
-                if (DEBUG) Log.i(TAG, "Matches possibly non-default WebApk");
+                if (debug()) Log.i(TAG, "Matches possibly non-default WebApk");
                 return true;
             }
         }
@@ -796,19 +797,19 @@
         // or not enforce it at all.
         if (!params.isRendererInitiated() && !incomingIntentRedirect && !isRedirectFromFormSubmit
                 && mDelegate.shouldEmbedderInitiatedNavigationsStayInBrowser()) {
-            if (DEBUG) Log.i(TAG, "Browser or Intent initiated and not a redirect");
+            if (debug()) Log.i(TAG, "Browser or Intent initiated and not a redirect");
             return false;
         }
 
         if (isFormSubmit && !incomingIntentRedirect && !isRedirectFromFormSubmit) {
-            if (DEBUG) Log.i(TAG, "Direct form submission, not a redirect");
+            if (debug()) Log.i(TAG, "Direct form submission, not a redirect");
             return false;
         }
 
         // http://crbug/331571 : Do not override a navigation started from user typing.
         if (params.getRedirectHandler() != null
                 && params.getRedirectHandler().isNavigationFromUserTyping()) {
-            if (DEBUG) Log.i(TAG, "Navigation from user typing");
+            if (debug()) Log.i(TAG, "Navigation from user typing");
             return false;
         }
         return true;
@@ -822,7 +823,7 @@
     private boolean isLinkFromChromeInternalPage(ExternalNavigationParams params) {
         if (params.getReferrerUrl().getScheme().equals(UrlConstants.CHROME_SCHEME)
                 && UrlUtilities.isHttpOrHttps(params.getUrl())) {
-            if (DEBUG) Log.i(TAG, "Link from an internal chrome:// page");
+            if (debug()) Log.i(TAG, "Link from an internal chrome:// page");
             return true;
         }
         return false;
@@ -837,7 +838,7 @@
         // wtai://wp/mc;number
         // number=string(phone-number)
         String phoneNumber = url.getSpec().substring(WTAI_MC_URL_PREFIX.length());
-        if (DEBUG) Log.i(TAG, "wtai:// link handled");
+        if (debug()) Log.i(TAG, "wtai:// link handled");
         RecordUserAction.record("Android.PhoneIntent");
         return new Intent(Intent.ACTION_VIEW, Uri.parse(WebView.SCHEME_TEL + phoneNumber));
     }
@@ -845,7 +846,7 @@
     private static boolean isUnhandledWtaiProtocol(ExternalNavigationParams params) {
         if (!params.getUrl().getSpec().startsWith(WTAI_URL_PREFIX)) return false;
         if (isSupportedWtaiProtocol(params.getUrl())) return false;
-        if (DEBUG) Log.i(TAG, "Unsupported wtai:// link");
+        if (debug()) Log.i(TAG, "Unsupported wtai:// link");
         return true;
     }
 
@@ -855,12 +856,12 @@
      */
     private boolean hasInternalScheme(GURL targetUrl, Intent targetIntent) {
         if (isInternalScheme(targetUrl.getScheme())) {
-            if (DEBUG) Log.i(TAG, "Navigating to a chrome-internal page");
+            if (debug()) Log.i(TAG, "Navigating to a chrome-internal page");
             return true;
         }
         if (UrlUtilities.hasIntentScheme(targetUrl) && targetIntent.getData() != null
                 && isInternalScheme(targetIntent.getData().getScheme())) {
-            if (DEBUG) Log.i(TAG, "Navigating to a chrome-internal page");
+            if (debug()) Log.i(TAG, "Navigating to a chrome-internal page");
             return true;
         }
         return false;
@@ -886,7 +887,7 @@
         } else {
             hasContentScheme = UrlConstants.CONTENT_SCHEME.equals(targetUrl.getScheme());
         }
-        if (DEBUG && hasContentScheme) Log.i(TAG, "Navigation to content: URL");
+        if (debug() && hasContentScheme) Log.i(TAG, "Navigation to content: URL");
         return hasContentScheme;
     }
 
@@ -908,7 +909,7 @@
         if (data == null || data.getScheme() == null) return false;
 
         if (data.getScheme().equalsIgnoreCase(UrlConstants.FILE_SCHEME)) {
-            if (DEBUG) Log.i(TAG, "Intent navigation to file: URI");
+            if (debug()) Log.i(TAG, "Intent navigation to file: URI");
             return true;
         }
         return false;
@@ -924,7 +925,7 @@
     protected boolean isYoutubePairingCode(GURL url) {
         if (url.domainIs("youtube.com")
                 && !TextUtils.isEmpty(UrlUtilities.getValueForKeyInQuery(url, "pairingCode"))) {
-            if (DEBUG) Log.i(TAG, "YouTube URL with a pairing code");
+            if (debug()) Log.i(TAG, "YouTube URL with a pairing code");
             return true;
         }
         return false;
@@ -939,7 +940,7 @@
         }
 
         if (mDelegate.shouldDisableExternalIntentRequestsForUrl(params.getUrl())) {
-            if (DEBUG) Log.i(TAG, "Delegate disables external intent requests for URL.");
+            if (debug()) Log.i(TAG, "Delegate disables external intent requests for URL.");
             return true;
         }
         return false;
@@ -953,7 +954,7 @@
         if (RedirectHandler.isRefactoringEnabled()) return false;
         if (params.getRedirectHandler() != null
                 && params.getRedirectHandler().isNavigationChainExpired()) {
-            if (DEBUG) {
+            if (debug()) {
                 Log.i(TAG,
                         "Navigation chain expired "
                                 + "(a page waited more than %d seconds to redirect).",
@@ -980,7 +981,7 @@
         // See RedirectHandler#NAVIGATION_CHAIN_TIMEOUT_MILLIS for details. We don't want an
         // unattended page to redirect to an app.
         if (handler.isNavigationChainExpired()) {
-            if (DEBUG) {
+            if (debug()) {
                 Log.i(TAG,
                         "Navigation chain expired "
                                 + "(a page waited more than %d seconds to redirect).",
@@ -992,20 +993,20 @@
         // If a navigation chain has used the history API to go back/forward external navigation is
         // probably not expected or desirable.
         if (handler.navigationChainUsedBackOrForward()) {
-            if (DEBUG) Log.i(TAG, "Navigation chain used back or forward.");
+            if (debug()) Log.i(TAG, "Navigation chain used back or forward.");
             return true;
         }
 
         // Used to prevent things like chaining fallback URLs.
         if (handler.shouldNotOverrideUrlLoading()) {
-            if (DEBUG) Log.i(TAG, "Navigation chain has blocked app launching.");
+            if (debug()) Log.i(TAG, "Navigation chain has blocked app launching.");
             return true;
         }
 
         // Tab Restores should definitely not launch apps, and refreshes launching apps would
         // probably not be expected or desirable.
         if (initialState.isFromReload) {
-            if (DEBUG) Log.i(TAG, "Navigation chain is from a tab restore or refresh.");
+            if (debug()) Log.i(TAG, "Navigation chain is from a tab restore or refresh.");
             return true;
         }
 
@@ -1018,7 +1019,7 @@
         if (!initialState.isRendererInitiated && !initialState.isFromIntent
                 && (mDelegate.shouldEmbedderInitiatedNavigationsStayInBrowser()
                         || initialState.isFromTyping)) {
-            if (DEBUG) Log.i(TAG, "Browser initiated navigation chain.");
+            if (debug()) Log.i(TAG, "Browser initiated navigation chain.");
             return true;
         }
 
@@ -1029,7 +1030,7 @@
         // If an intent targeted Chrome explicitly, we assume the app wanted to launch Chrome and
         // not another app.
         if (handler.intentPrefersToStayInChrome() && !isExternalProtocol) {
-            if (DEBUG) Log.i(TAG, "Launching intent explicitly targeted the browser.");
+            if (debug()) Log.i(TAG, "Launching intent explicitly targeted the browser.");
             return true;
         }
 
@@ -1039,7 +1040,7 @@
         if (initialState.isRendererInitiated && !initialState.hasUserGesture
                 && !initialState.isFromFormSubmit) {
             if (isExternalProtocol) handler.maybeLogExternalRedirectBlockedWithMissingGesture();
-            if (DEBUG) Log.i(TAG, "Navigation chain started without a gesture.");
+            if (debug()) Log.i(TAG, "Navigation chain started without a gesture.");
             return true;
         }
         return false;
@@ -1060,7 +1061,7 @@
         int pageTransitionCore = params.getPageTransition() & PageTransition.CORE_MASK;
         boolean isFormSubmit = pageTransitionCore == PageTransition.FORM_SUBMIT;
         if (isFormSubmit) {
-            if (DEBUG) Log.i(TAG, "Direct form submission, not a redirect");
+            if (debug()) Log.i(TAG, "Direct form submission, not a redirect");
             return true;
         }
         return false;
@@ -1082,7 +1083,7 @@
         // Redirects off of intents are still allowed to launch apps (eg. URL shorteners).
         if (incomingIntentRedirect) return false;
 
-        if (DEBUG) Log.i(TAG, "Initial intent navigation.");
+        if (debug()) Log.i(TAG, "Initial intent navigation.");
         return true;
     }
 
@@ -1099,7 +1100,7 @@
             return handleWithMarketIntent(params, targetIntent);
         }
 
-        if (DEBUG) Log.i(TAG, "Could not find an external activity to use");
+        if (debug()) Log.i(TAG, "Could not find an external activity to use");
         return OverrideUrlLoadingResult.forNoOverride();
     }
 
@@ -1139,7 +1140,7 @@
     private boolean shouldStayInIncognito(
             ExternalNavigationParams params, boolean isExternalProtocol) {
         if (params.isIncognito() && !isExternalProtocol) {
-            if (DEBUG) Log.i(TAG, "Stay incognito");
+            if (debug()) Log.i(TAG, "Stay incognito");
             return true;
         }
         return false;
@@ -1152,7 +1153,7 @@
         if (params.isIncognito()) return false;
         if (mDelegate.maybeLaunchInstantApp(params.getUrl(), params.getReferrerUrl(),
                     incomingIntentRedirect, isSerpReferrer(), resolveInfos)) {
-            if (DEBUG) Log.i(TAG, "Launching instant App.");
+            if (debug()) Log.i(TAG, "Launching instant App.");
             return true;
         }
         return false;
@@ -1163,7 +1164,7 @@
      * specialized external app handling it.
      */
     private OverrideUrlLoadingResult fallBackToHandlingInApp() {
-        if (DEBUG) Log.i(TAG, "No specialized handler for URL");
+        if (debug()) Log.i(TAG, "No specialized handler for URL");
         return OverrideUrlLoadingResult.forNoOverride();
     }
 
@@ -1253,7 +1254,7 @@
         previousIntent.setData(Uri.parse(previousUrl.getSpec()));
 
         if (resolversSubsetOf(resolvingInfos, queryIntentActivities(previousIntent))) {
-            if (DEBUG) Log.i(TAG, "Same host, no new resolvers");
+            if (debug()) Log.i(TAG, "Same host, no new resolvers");
             return true;
         }
         return false;
@@ -1266,7 +1267,7 @@
     private boolean preventDirectInstantAppsIntent(Intent intent) {
         if (!isIntentToInstantApp(intent)) return false;
         if (isSerpReferrer() && mDelegate.handlesInstantAppLaunchingInternally()) return false;
-        if (DEBUG) Log.i(TAG, "Intent URL to an Instant App");
+        if (debug()) Log.i(TAG, "Intent URL to an Instant App");
         RecordHistogram.recordEnumeratedHistogram("Android.InstantApps.DirectInstantAppsIntent",
                 AiaIntent.OTHER, AiaIntent.NUM_ENTRIES);
         return true;
@@ -1314,11 +1315,11 @@
         // to external apps.
         if (startIncognitoIntent(
                     params, targetIntent, browserFallbackUrl, shouldProxyForInstantApps)) {
-            if (DEBUG) Log.i(TAG, "Incognito navigation out");
+            if (debug()) Log.i(TAG, "Incognito navigation out");
             return OverrideUrlLoadingResult.forAsyncAction(
                     OverrideUrlLoadingAsyncActionType.UI_GATING_INTENT_LAUNCH);
         }
-        if (DEBUG) Log.i(TAG, "Failed to show incognito alert dialog.");
+        if (debug()) Log.i(TAG, "Failed to show incognito alert dialog.");
         return OverrideUrlLoadingResult.forNoOverride();
     }
 
@@ -1454,7 +1455,7 @@
                 && !params.getRedirectHandler().isFromCustomTabIntent()
                 && !params.getRedirectHandler().hasNewResolver(
                         resolvingInfos, (Intent intent) -> queryIntentActivities(intent))) {
-            if (DEBUG) Log.i(TAG, "Intent navigation with no new handlers.");
+            if (debug()) Log.i(TAG, "Intent navigation with no new handlers.");
             return true;
         }
         return false;
@@ -1486,7 +1487,7 @@
         for (ResolveInfo resolveInfo : getResolveInfosForWebApks(params, resolvingInfos)) {
             ActivityInfo info = resolveInfo.activityInfo;
             if (info != null && currentName.equals(info.packageName)) {
-                if (DEBUG) Log.i(TAG, "Already in WebAPK");
+                if (debug()) Log.i(TAG, "Already in WebAPK");
                 return true;
             }
         }
@@ -1515,14 +1516,14 @@
                             == 1);
             switch (intentAllowingAppResult) {
                 case IntentToAutofillAllowingAppResult.DEFER_TO_APP_NOW:
-                    if (DEBUG) {
+                    if (debug()) {
                         Log.i(TAG, "Autofill Assistant passed in favour of App.");
                     }
                     return false;
                 case IntentToAutofillAllowingAppResult.DEFER_TO_APP_LATER:
                     if (params.getRedirectHandler() != null
                             && isAutofillAssistantGoogleReferrer(params)) {
-                        if (DEBUG) {
+                        if (debug()) {
                             Log.i(TAG, "Autofill Assistant passed in favour of App later.");
                         }
                         params.getRedirectHandler()
@@ -1537,9 +1538,9 @@
 
         if (mDelegate.handleWithAutofillAssistant(params, targetIntent, browserFallbackUrl,
                     isAutofillAssistantGoogleReferrer(params))) {
-            if (DEBUG) Log.i(TAG, "Handled with Autofill Assistant.");
+            if (debug()) Log.i(TAG, "Handled with Autofill Assistant.");
         } else {
-            if (DEBUG) Log.i(TAG, "Not handled with Autofill Assistant.");
+            if (debug()) Log.i(TAG, "Not handled with Autofill Assistant.");
         }
         return true;
     }
@@ -1764,7 +1765,7 @@
         // Android disambiguation prompt.
         boolean result = !resolversSubsetOf(
                 Collections.singletonList(resolveActivity), possibleHandlingActivities);
-        if (DEBUG && result) Log.i(TAG, "Avoiding disambiguation dialog.");
+        if (debug() && result) Log.i(TAG, "Avoiding disambiguation dialog.");
         return result;
     }
 
@@ -1833,21 +1834,21 @@
 
         if (!deviceCanHandleIntent(intent)) {
             // Exit early if the Play Store isn't available. (https://crbug.com/820709)
-            if (DEBUG) Log.i(TAG, "Play Store not installed.");
+            if (debug()) Log.i(TAG, "Play Store not installed.");
             return OverrideUrlLoadingResult.forNoOverride();
         }
 
         if (params.isIncognito()) {
             if (!startIncognitoIntent(params, intent, fallbackUrl, false)) {
-                if (DEBUG) Log.i(TAG, "Failed to show incognito alert dialog.");
+                if (debug()) Log.i(TAG, "Failed to show incognito alert dialog.");
                 return OverrideUrlLoadingResult.forNoOverride();
             }
-            if (DEBUG) Log.i(TAG, "Incognito intent to Play Store.");
+            if (debug()) Log.i(TAG, "Incognito intent to Play Store.");
             return OverrideUrlLoadingResult.forAsyncAction(
                     OverrideUrlLoadingAsyncActionType.UI_GATING_INTENT_LAUNCH);
         } else {
             startActivity(intent, false);
-            if (DEBUG) Log.i(TAG, "Intent to Play Store.");
+            if (debug()) Log.i(TAG, "Intent to Play Store.");
             return OverrideUrlLoadingResult.forExternalIntent();
         }
     }
@@ -1917,12 +1918,12 @@
         webApkIntent.setPackage(packageName);
         try {
             startActivity(webApkIntent, false);
-            if (DEBUG) Log.i(TAG, "Launched WebAPK");
+            if (debug()) Log.i(TAG, "Launched WebAPK");
             return true;
         } catch (ActivityNotFoundException e) {
             // The WebApk must have been uninstalled/disabled since we queried for Activities to
             // handle this intent.
-            if (DEBUG) Log.i(TAG, "WebAPK launch failed");
+            if (debug()) Log.i(TAG, "WebAPK launch failed");
             return false;
         }
     }
@@ -2117,7 +2118,7 @@
         } catch (ActivityNotFoundException e) {
             // The targeted app must have been uninstalled/disabled since we queried for Activities
             // to handle this intent.
-            if (DEBUG) Log.i(TAG, "Activity not found.");
+            if (debug()) Log.i(TAG, "Activity not found.");
         } catch (AndroidRuntimeException e) {
             // https://crbug.com/1226177: Most likely cause of this exception is Android failing
             // to start the app that we previously detected could handle the Intent.
@@ -2131,7 +2132,7 @@
     }
 
     private OverrideUrlLoadingResult doStartActivity(Intent intent, Context context) {
-        if (DEBUG) Log.i(TAG, "startActivity");
+        if (debug()) Log.i(TAG, "startActivity");
         context.startActivity(intent);
         recordExternalNavigationDispatched(intent);
         return OverrideUrlLoadingResult.forExternalIntent();
@@ -2246,7 +2247,7 @@
 
         // No app can resolve the intent, don't prompt.
         if (intentResolveInfo == null || intentResolveInfo.activityInfo == null) {
-            if (DEBUG) Log.i(TAG, "Message doesn't resolve to any app.");
+            if (debug()) Log.i(TAG, "Message doesn't resolve to any app.");
             return OverrideUrlLoadingResult.forNoOverride();
         }
 
@@ -2258,7 +2259,7 @@
         // to block the navigation, and sites hoping to prompt the user when navigation fails should
         // make sure to correctly target their app.
         if (!resolversSubsetOf(Arrays.asList(intentResolveInfo), resolvingInfos.get())) {
-            if (DEBUG) Log.i(TAG, "Message resolves to multiple apps.");
+            if (debug()) Log.i(TAG, "Message resolves to multiple apps.");
             return OverrideUrlLoadingResult.forNoOverride();
         }
 
@@ -2266,7 +2267,7 @@
                 MessageDispatcherProvider.from(mDelegate.getWindowAndroid());
         WebContents webContents = mDelegate.getWebContents();
         if (messageDispatcher == null || webContents == null) {
-            if (DEBUG) Log.i(TAG, "No WebContents to show Message for.");
+            if (debug()) Log.i(TAG, "No WebContents to show Message for.");
             return OverrideUrlLoadingResult.forNoOverride();
         }
 
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 0ea44b7..f532236 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
@@ -34,6 +34,7 @@
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
@@ -48,6 +49,7 @@
 import org.chromium.base.supplier.Supplier;
 import org.chromium.base.test.BaseJUnit4ClassRunner;
 import org.chromium.base.test.util.Batch;
+import org.chromium.base.test.util.Features;
 import org.chromium.base.test.util.MaxAndroidSdkLevel;
 import org.chromium.base.test.util.MinAndroidSdkLevel;
 import org.chromium.components.external_intents.ExternalNavigationDelegate.IntentToAutofillAllowingAppResult;
@@ -76,6 +78,7 @@
 @RunWith(BaseJUnit4ClassRunner.class)
 // clang-format off
 @Batch(Batch.UNIT_TESTS)
[email protected](ExternalIntentsFeatures.EXTERNAL_NAVIGATION_DEBUG_LOGS_NAME)
 public class ExternalNavigationHandlerTest {
     // clang-format on
     // Expectations
@@ -181,6 +184,9 @@
     @Rule
     public MockitoRule mMockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
 
+    @Rule
+    public TestRule mFeaturesProcessorRule = new Features.JUnitProcessor();
+
     @Mock
     AlertDialog mAlertDialog;
 
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index def7129..b80b049 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -57288,6 +57288,7 @@
   <int value="-1301804101"
       label="AutofillEnableInfoBarAccountIndicationFooterForSingleAccountUsers:disabled"/>
   <int value="-1301167148" label="WebViewZeroCopyVideo:disabled"/>
+  <int value="-1300934428" label="ExternalNavigationDebugLogs:enabled"/>
   <int value="-1298273481" label="http2-grease-settings"/>
   <int value="-1298067767" label="CalendarModelDebugMode:disabled"/>
   <int value="-1297079591" label="EnableRemovingAllThirdPartyCookies:disabled"/>
@@ -60644,6 +60645,7 @@
   <int value="787080596" label="DynamicTcmallocTuning:enabled"/>
   <int value="787385958" label="RegionalLocalesAsDisplayUI:enabled"/>
   <int value="788130042" label="PrivacySandboxSettings2:enabled"/>
+  <int value="789654228" label="ExternalNavigationDebugLogs:disabled"/>
   <int value="790411499" label="PageInfoStoreInfo:disabled"/>
   <int value="791541863" label="NtpPhotosModule:disabled"/>
   <int value="791979491"