Skip to content

Browser.OpenAsync(External) on Android throws FeatureNotSupportedException for verified App Link owner URLs even with documented <queries> fix applied #35651

Description

@Kebechet

Description

Browser.OpenAsync(uri, BrowserLaunchMode.External) throws FeatureNotSupportedException ("Specified method is not supported.") on Android when the URL belongs to a verified App Link owner that the calling app cannot see due to Android 11+ package visibility — even when the documented <queries> declaration for http/https is correctly applied.

This is not the same as #20182 / #27744. Those were closed by pointing users at the <queries> declaration, which fixes the "fall through to a generic browser" subset of cases. It does not fix the App Link owner case (Instagram, Facebook, Spotify, X, TikTok, Google Maps when it owns a domain, etc.), which is the larger and more user-visible class of failures.

Steps to Reproduce

  1. Create a File > New .NET MAUI app, target net10.0-android, min SDK 23+.
  2. Add the documented <queries> block to Platforms/Android/AndroidManifest.xml:
    <queries>
      <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="https" />
      </intent>
      <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="http" />
      </intent>
    </queries>
  3. On the device, install Instagram (or any verified App Link owner: Facebook, Spotify, X, TikTok…).
  4. Call:
    await Browser.Default.OpenAsync(
        new Uri("https://www.instagram.com/instagram/"),
        BrowserLaunchMode.External);

Expected: Instagram opens at the target profile (the OS deep-links via verified App Link, the same way Chrome would).

Actual: Microsoft.Maui.ApplicationModel.FeatureNotSupportedException: Specified method is not supported.

at Microsoft.Maui.ApplicationModel.BrowserImplementation.LaunchExternalBrowser(...) in Browser.android.cs:line 90
at Microsoft.Maui.ApplicationModel.BrowserImplementation.OpenAsync(...) in Browser.android.cs:line 23

Root Cause

In src/Essentials/src/Browser/Browser.android.cs:

static void LaunchExternalBrowser(BrowserLaunchOptions options, AndroidUri? nativeUri)
{
    var intent = new Intent(Intent.ActionView, nativeUri);
    ...
    intent.SetFlags(flags);

    if (!PlatformUtils.IsIntentSupported(intent))      // ← problem
        throw new FeatureNotSupportedException();

    Application.Context.StartActivity(intent);
}

PlatformUtils.IsIntentSupported(intent) is intent.ResolveActivity(pm), which is filtered by caller-side package visibility. StartActivity, by contrast, dispatches via the system intent resolver and is not subject to caller-side visibility — it can launch invisible activities; the caller just isn't allowed to query about them ahead of time.

So ResolveActivity returns null in two distinct situations:

  1. No handler exists at all. StartActivity would also fail (ActivityNotFoundException).
  2. A handler exists but is invisible to the caller due to package visibility. StartActivity would succeed.

Today MAUI conflates the two and throws even in case 2, blocking a launch that Android can perform.

Why the <queries> Fix Doesn't Cover This

Per Android's auto-visibility rules:

"If the intent filter includes a <data> element that contains a host, then your app is NOT considered to handle a web intent."

A standard <queries><intent VIEW + scheme=https></intent></queries> declaration matches generic browsers (Chrome, Firefox, Edge — no host restriction on their VIEW filter) but does not match host-bound App Link owners (IG, FB, Spotify — host="instagram.com", etc.).

Empirical confirmation via adb shell dumpsys package queries on a Pixel running stock Android 14 with the queries block applied:

app.example:
  com.android.chrome       ← visible (browser, no host filter)
  com.opera.browser        ← visible
  com.microsoft.emmx       ← visible
  com.google.android.apps.maps
  ... (no com.instagram.android, no com.facebook.katana, etc.)

Logcat at IG-link launch time:

AppsFilter: interaction: PackageSetting{... app.example} -> PackageSetting{... com.instagram.android} BLOCKED

The verified App Link owner has exclusive ownership of instagram.com once verification succeeds, so Android does not fall back to a visible browser — ResolveActivity returns null, MAUI throws, nothing launches.

Why the Closed Issues Don't Cover This

The two closed issues both leave the App Link owner installed and verified case silently broken. That is the case most apps hit in production because it covers nearly every popular consumer app users have installed (IG, FB, Spotify, X, TikTok, WhatsApp click-to-chat, YouTube, Reddit, etc.).

Workarounds (and what they prove)

  1. Switch to BrowserLaunchMode.SystemPreferred — works. That path has no IsIntentSupported pre-check; CustomTabsIntent.LaunchUrl ultimately calls ContextCompat.startActivity with an ACTION_VIEW intent, the system resolver picks the App Link owner, and IG opens. Side effect: non-deep-link URLs open in an in-app Chrome Custom Tab rather than the user's default browser. Acceptable for many apps, but it's a behavior change for those wanting true "external" semantics.
  2. Bypass MAUI's Browser on Android — issue new Intent(Intent.ActionView, uri).SetFlags(NewTask) + StartActivity directly, with try/catch for ActivityNotFoundException. Confirmed by the workaround snippet in [Essentials] Using Browser.OpenAsync with BrowserLaunchMode.External to open universal Google Maps URL throws FeatureNotSupportedException when Google Maps app disabled on Android #27744.

Both workarounds confirm the launch is fundamentally fine — only MAUI's pre-check is wrong.

Proposed Fixes

Option A — Remove the pre-check, rely on Android's ActivityNotFoundException (recommended; PR follows)

static void LaunchExternalBrowser(BrowserLaunchOptions options, AndroidUri? nativeUri)
{
    var intent = new Intent(Intent.ActionView, nativeUri);
    var flags = ActivityFlags.ClearTop | ActivityFlags.NewTask;

#if __ANDROID_24__
    if (OperatingSystem.IsAndroidVersionAtLeast(24))
    {
        if (options.HasFlag(BrowserLaunchFlags.LaunchAdjacent))
            flags |= ActivityFlags.LaunchAdjacent;
    }
#endif
    intent.SetFlags(flags);

    try
    {
        Application.Context.StartActivity(intent);
    }
    catch (ActivityNotFoundException ex)
    {
        throw new FeatureNotSupportedException(
            "No activity found to handle URI: " + nativeUri, ex);
    }
}

ActivityNotFoundException is the only authoritative signal that no activity can actually handle the intent — because only the system dispatcher knows, and the system dispatcher ignores caller-side visibility. The public contract (FeatureNotSupportedException thrown when no activity is available) is preserved; we just wrap the real Android exception instead of guessing from a visibility-filtered query.

Option B — Skip the pre-check only for web schemes (matches the maintainer's own suggestion)

var scheme = nativeUri?.Scheme;
var isWeb = scheme is "http" or "https";

if (!isWeb && !PlatformUtils.IsIntentSupported(intent))
    throw new FeatureNotSupportedException();

try
{
    Application.Context.StartActivity(intent);
}
catch (ActivityNotFoundException ex)
{
    throw new FeatureNotSupportedException(
        "No activity found to handle URI: " + nativeUri, ex);
}

Rationale: for http/https, a browser is essentially always installed and always visible (browsers have host-less VIEW filters → auto-visible per Android's rules). The only thing the pre-check can filter out for web URLs is an App Link owner — which is exactly the case we want to launch. For non-web schemes (custom-scheme apps), the pre-check is kept as cheap correctness insurance against truly unhandled intents.

This matches @jfversluis's own comment on #27744: "if its http(s) we should just open the browser and not do anything further."

Affected

  • Platform: Android only (iOS/Mac Catalyst/Windows unaffected).
  • Android version: API 30+ (package visibility is enforced from Android 11).
  • MAUI version: reproduced on 10.0.51. The affected code in Browser.android.cs has been unchanged since the initial Essentials port, so all currently-supported MAUI versions are likely affected.

Note on duplicate detection

This is intentionally filed as a new issue rather than a reopen of #20182 / #27744 because:

  • Both closed-out resolutions point to <queries>, which does not address this case (the report would be lost in the closed-issue triage queue).
  • The root cause and proposed fix are different from what those threads concluded.
  • The reproduction scenario (verified App Link owner) is not covered in either of their reproductions.

Happy to consolidate / cross-link if the team would prefer one of those reopened instead.

I'm preparing a PR with Option A; happy to switch to Option B if the team prefers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-essentialsEssentials: Device, Display, Connectivity, Secure Storage, Sensors, App Infoplatform/androids/triagedIssue has been revieweds/verifiedVerified / Reproducible Issue ready for Engineering Triage

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions