Path attributes, in order of how often you'll need them
Android gives you four ways to specify which paths an intent filter should match:
path, pathPrefix, pathPattern, and
pathSuffix. They're mutually exclusive on a given <data>
element, and picking the wrong one is the single most common cause of "my
deep link works on Stack Overflow's example but not mine." In a typical
app you'll mostly use pathPrefix, occasionally reach for
pathPattern, and rarely use the other two.
android:pathPrefix
Matches any path starting with the given string. pathPrefix="/products"
matches /products, /products/123,
/products/abc/details, and /products?id=42. This is what you
want for the overwhelming majority of deep links. Anytime your activity handles a
"section" of your site (all product pages, all user profiles, all article URLs),
pathPrefix is the answer. If you're not sure which to use and your URL
pattern starts with a fixed string, this is it.
android:path
Exact-match against one specific path, character for character. Use when an activity
only handles a single URL. Typical examples are /legal/privacy or
/.well-known/apple-app-site-association. If you find yourself listing
five different path attributes in an intent filter, that's a smell.
pathPrefix is almost certainly cleaner.
android:pathPattern
Matches paths against a glob-like pattern. Useful when the variable part of the URL is
in the middle, not at the end. For example, /users/<id>/posts
matches /users/42/posts and /users/abc/posts but not
/users/42/profile. The pattern syntax is where most people get tripped up.
pathPattern is not a full regex. Android uses a limited glob called
PATTERN_SIMPLE_GLOB, and the rules are unintuitive enough that they're
worth memorising.
.matches any single character, including a literal dot. There's no way to distinguish them without escaping.*matches zero or more occurrences of the preceding character. Soa*matches "", "a", "aa", "aaa".*by itself does not mean "any string." That's a regex thing, not a glob thing..*together means "any sequence of characters." This is the closest equivalent to the regex wildcard most people are reaching for.\\*matches a literal*.\\.matches a literal.. And yes, you need the double backslash because XML eats one of them on the way through the parser.
The practical implication: a pathPattern like /api/v.*
matches more than you think, because the . after v matches
any character, not just a literal dot. If you mean "v" followed by digits, you'd
need a more specific pattern, but the glob doesn't support character classes, so
usually pathPrefix="/api/v" is the cleaner answer anyway.
android:pathSuffix (Android 12+)
Matches paths ending with the given string. Added in API level 31 (Android 12),
which makes it useless if your minSdkVersion is below 31. The attribute
will silently fail to match on older devices. Useful for things like
pathSuffix=".pdf" to handle any PDF URL, but if you need older-device
support, back-fill with pathPattern=".*\\.pdf".
android:autoVerify and the App Links flow
Setting android:autoVerify="true" on an intent filter for HTTPS URLs
declares that your app wants to automatically handle those links instead of
competing with the browser via the standard "Open with" picker. To make this work,
you also need:
- An
assetlinks.jsonfile athttps://yourdomain.com/.well-known/assetlinks.json, served asapplication/json, containing your app's package name and signing-key SHA-256 fingerprints. - Your
<data>element to havescheme="https"(or bothhttpandhttps). Auto-verification doesn't apply to other schemes. - Patience. Android verifies on app install in the background and on every update, but it can take seconds to minutes, and it can fail silently if anything is misconfigured.
If verification fails, the user sees the standard disambiguation chooser ("Open with…") instead of your app launching directly. Many users will then click the browser, not your app. Diagnose with:
adb shell pm get-app-links com.example.app
The output shows the verification state per host. Anything other than
verified means your assetlinks.json isn't right.
The multiple-<data>-elements gotcha
If your intent filter has more than one <data> element, Android
treats their attributes as a cross-product, not as separate filters. This
means:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" android:host="example.com" />
<data android:scheme="myapp" android:host="other.com" />
</intent-filter>
...does not mean "match https://example.com OR
myapp://other.com." It means "match any combination of the listed
schemes and the listed hosts." So it also matches https://other.com and
myapp://example.com, which you probably didn't intend. If you
want different scheme-host pairs to match, use separate
<intent-filter> elements for each.
The BROWSABLE + DEFAULT requirement
For an intent filter to handle deep links arriving from external sources (browsers, messengers, other apps), it needs both:
android.intent.category.BROWSABLE: declares the activity can be invoked from a browser or other external context.android.intent.category.DEFAULT: required for any implicit intent. Without this, your activity won't match implicit deep link intents at all, even if everything else is right.
People skip DEFAULT sometimes because their activity also has the
LAUNCHER category and works fine "for testing." Then in production,
deep links from Gmail or Instagram silently fail to open the app and they spend
hours debugging the wrong things.
Android 12+ android:exported
Since Android 12 (API 31), any activity with at least one intent filter must
explicitly declare android:exported="true", or the build fails. The
tool above includes android:exported="true" when generating an activity
wrapper for HTTPS or BROWSABLE intent filters. The value is required for these
intent filters to work, and most modern apps handling deep links will want it set.
Subdomain wildcards aren't supported
A common request: "I want my intent filter to match *.example.com, for
every subdomain." Android does not support host wildcards in this
form. The android:host attribute is matched literally. Workarounds:
- List each subdomain explicitly with separate
<intent-filter>elements (one per host). - Use a leading
*.only as the very first character. Android does support exactly*.example.comas a leading wildcard host, but support is inconsistent across versions and OEMs. Test before relying on it. - For App Links, list every host you intend to verify in your
assetlinks.jsonand in your manifest. Painful but reliable.
What goes wrong, repeatedly
The single most common failure is forgetting BROWSABLE. The activity
launches fine when you invoke it programmatically with adb am start, or
when you wire it to a button in another app. Then a real user taps a link in Chrome
or Gmail and nothing happens. This shows up especially when people copy
intent-filter snippets from Stack Overflow that were written for a different use
case. A share-target activity, for example, doesn't need BROWSABLE. A
deep-link target does.
Reaching for pathPattern when pathPrefix would do is the
next-most-common mistake, and it tends to bite worse because the failure is weirder.
pathPattern's . wildcard matches more than you expect, and
a pattern like /v.* doesn't mean "/v followed by a version number" the
way you assumed. If you don't have a variable in the middle of your URL, prefer
pathPrefix. It's exact and predictable.
Case sensitivity catches people too. Path attributes are case-sensitive.
/Products and /products are different patterns. Hosts are
case-insensitive (matched lowercased), but the path matching is strict. If your
backend serves both /products and /Products (some CMSes
do, irritatingly), you may need both as separate filters.
Setting autoVerify on a non-HTTPS scheme is silently ignored, and the
silence is the problem. There's no build warning, no log message, nothing. The
attribute only does anything when paired with scheme="http" or
scheme="https". If you've set it on a custom scheme and wondered why
auto-verification never happens, that's why.
Mixing a custom scheme and HTTPS in one intent filter is technically valid but
rarely what you want. The filter matches both schemes, but autoVerify
doesn't apply to either, because the attribute requires an HTTPS-only filter.
Better to have two separate filters: one for your custom scheme without
autoVerify, one for HTTPS with it. The duplication is the price of
getting App Links verification to actually fire.