Intents and filters
An Intent is either a named component or a question about capabilities. The difference between those two forms is the difference between a phone call and a noticeboard.
When you tap a link in a chat app and a browser opens, no code in either app knows about the other. One app described an operation; the system found somebody who could perform it. That indirection is what an is for.
TL;DR#
- An Intent carries an action, optional data (a URI and/or MIME type), categories, extras and flags.
- An explicit intent names a component and goes straight there. An implicit intent describes what you want and lets the system resolve it.
- Resolution compares the intent against every app's intent filters. Action, category and data must all agree; a filter requiring a category you did not set will not match.
- Several matches mean a chooser; a single match opens directly, unless a default has been set.
- Since Android 11, an app sees only a filtered view of installed packages, so resolving an implicit intent can fail for an app you cannot see.1
Mental model#
An explicit intent is a phone call to an extension: you know the person, you dial the number, the call connects or it does not.
An implicit intent is a noticeboard posting: "Wanted — someone who can show a web page." Whoever has registered that capability may answer. You do not know, and cannot know, who will.
Where the analogy breaks#
The noticeboard has a gatekeeper who also does the matching: the system does not broadcast your request and let everyone volunteer. It computes the exact set of matching filters before anything is started, applies ranking and defaults, and only then delivers one intent. That is why the answer can change when you install an app, set a default, or switch to a work profile — the match is computed fresh, not cached by you.
The big picture#
Text description of this diagram
An interactive intent resolver. The left side builds an intent from an action, categories, a URI (scheme, host, path) and a MIME type. Five sample filters are shown: a browser that accepts https and http with the BROWSABLE category, a maps app that accepts geo and one specific https host, a music app that accepts content and file URIs with audio MIME types, a camera app that only accepts IMAGE_CAPTURE with no data, and a notes app that accepts SEND with text/plain. Each filter is tested attribute by attribute — action, then categories, then scheme, host and MIME type — and the trace explains the first test that failed. Filters that survive every test are the candidates, and the count of candidates determines whether the intent opens directly, shows a chooser, or finds no app at all.
Change the action to SEND, then add text/plain as the type: the notes filter starts matching. Remove
the categories and watch the browser filter fail, because it requires BROWSABLE. The lesson is
mechanical — three lists must agree — but the consequences are not, because those lists are written
by other people.
Deep dive#
Anatomy#
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, "Reading about intents")
putExtra(Intent.EXTRA_SUBJECT, "Zygote")
addCategory(Intent.CATEGORY_DEFAULT)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
startActivity(Intent.createChooser(intent, "Share via"))Every field has a job:
| Field | What it does | Gotcha |
|---|---|---|
| action | the verb, e.g. ACTION_VIEW | matched exactly; no inheritance |
| data | a Uri and/or a MIME type | setting one can clear the other |
| categories | extra requirements | the filter's categories must all be present in the intent |
| extras | a Bundle of payload | invisible to resolution |
| flags | behaviour for the launch | FLAG_ACTIVITY_* and FLAG_GRANT_* |
Extras deserve a warning. They are not part of matching, so an app can match your intent and then
crash on getStringExtra because the extra you sent was not the one it expects. The contract for
extras is documentation, not enforcement.
Explicit versus implicit#
// Explicit: the component is named. No filter is consulted.
val explicit = Intent(context, SettingsActivity::class.java)
// Implicit: the system resolves it.
val implicit = Intent(Intent.ACTION_DIAL, Uri.parse("tel:5550100"))Explicit intents still pass a permission check — the target must be exported or you must share its UID — but they skip matching entirely. That is why internal navigation inside an app is cheap and why an explicit intent to another app's non-exported component throws.
What resolution actually compares#
PackageManagerService keeps a resolver index built from every installed app's <intent-filter>
blocks. For an implicit intent it tests, in order:
- Action. The filter must declare the intent's action exactly.
- Categories. Every category in the filter must be present in the intent. The reverse is not
required, which is why the system adds
CATEGORY_DEFAULTto implicit intents: filters commonly require it. - Data. Scheme, host, port, path and MIME type. A filter that declares no data only matches an intent with no data.
The MIME rule has one asymmetry worth remembering: a filter's type may be more specific than the
intent's. A filter declaring text/plain matches an intent with type text/*; a filter declaring
text/* does not match an intent asking for text/plain. This is the opposite of how most people
improvise it, and it is why wildcards belong in the intent only when you genuinely want everything.
App links: when a URL should open your app#
A plain VIEW filter for https makes your app one of many possible browsers. An App Link is a
verified claim: your app declares the host and publishes an assetlinks.json file on that domain
naming its signing certificate. The system checks the file, and only then does the link open your app
directly instead of showing a chooser.2
<activity android:name=".ArticleActivity" android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="zygote.example" />
</intent-filter>
</activity>The autoVerify flag asks the system to perform that check at install time. Without verification you
have a deep link — a URI that may open your app — not an App Link, which is a verified one. The
distinction matters because only verified links skip the chooser.
PendingIntent: an intent someone else will send#
A wraps an Intent and hands it to another process, which can fire it later as your
app. It is how notifications launch your activity and how AlarmManager wakes your receiver.
Because it is a capability granted to someone else, it must be explicit about mutability. Since
Android 12, targeting apps must pass FLAG_IMMUTABLE or FLAG_MUTABLE when creating one; the
platform refuses to guess.3 API 31Android 12
val open = PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
},
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)Choose immutable unless a system component genuinely needs to fill in extras — smart reply, for
example. A mutable PendingIntent with an implicit inner intent is a well-known way to let another
app redirect your capability, which is why the platform also rejects implicit intents inside
PendingIntents for targeting apps.
Testing an intent#
An intent is data, so it can be asserted on without a device:
@Test
fun shareIntent_grantsReadPermission() {
val uri = Uri.parse("content://com.example.notes/notes/1")
val intent = buildShareIntent(uri)
assertEquals(Intent.ACTION_SEND, intent.action)
assertEquals("text/plain", intent.type)
assertTrue(intent.flags and Intent.FLAG_GRANT_READ_URI_PERMISSION != 0)
assertEquals(uri, intent.getParcelableExtra(Intent.EXTRA_STREAM))
}That is the reason to build intents in a function rather than inline at the call site. An inline intent can only be tested by launching it; a function returns a value that can be asserted on, and the assertions are the security properties you actually care about.
Intents across boundaries#
Intents are Parcelable, so they cross Binder. The whole intent — extras included — is serialised
into the transaction buffer, which has a hard size limit shared with everything else in the call.
Putting a bitmap or a large list into an extra is how TransactionTooLargeException happens. Pass a
URI and a permission grant instead.
// Do this: hand over a reference, plus a temporary read grant.
intent.data = contentUri
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)Sharing a file, correctly#
The most common intent mistake is sharing a file by path. Since Android 7, a file:// URI in an intent
throws FileUriExposedException, because the receiving app has no way to read it and the path leaks
your internal storage layout.
The correct sequence is three steps:
// 1. Write the file into a directory the FileProvider declares.
val file = File(cacheDir, "export.csv").apply { writeText(csv) }
// 2. Get a content:// URI from the provider.
val uri = FileProvider.getUriForFile(context, "${context.packageName}.files", file)
// 3. Grant read access for this intent only.
val share = Intent(Intent.ACTION_SEND).apply {
type = "text/csv"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}FileProvider is a ContentProvider in AndroidX whose only job is to map a private file to a
content:// URI with a grant. The file_paths.xml resource decides which directories it may expose,
which is the security boundary.
Behaviour changes that bite#
- Android 11 (API 30): package visibility.
queryIntentActivitiesreturns only packages your app can see, unless you declare<queries>or hold a relevant permission.1 - Android 12 (API 31):
PendingIntentmutability becomes mandatory;android:exportedbecomes mandatory on components with filters.3 - Android 13 (API 33): the photo picker and per-app language arrive, both surfaced through system intents rather than new APIs you call directly.4
- Android 16 (API 36): predictive back is on by default for targeting apps, which changes what the back gesture does after an intent-driven navigation.5
How it evolved#
Intents have been in Android since API 1. What changed is that the system became more careful about who may resolve them.
| API | Version | What changed |
|---|---|---|
| 18 | 4.3 Jelly Bean |
|
| 23 | 6.0 Marshmallow |
|
| 28 | 9 Pie |
|
| 30 | 11 |
|
| 31 | 12 |
|
| 33 | 13 Tiramisu |
|
| 37 | 17 Cinnamon Bun |
|
The direction of travel is consistent: implicit intents remain the flexible mechanism, but each
release narrows the set of things an implicit intent can silently reach — background starts,
package visibility, mutable PendingIntents.
Under the hood#
- Intent.javathe Intent class, its flags and its parcel format
- ComputerEngine.javawhere intent resolution queries are evaluated against the filter index
- PendingIntentRecord.javathe system_server record that backs a PendingIntent
- PackageManager.javathe public resolution API: resolveActivity, queryIntentActivities
Curiosity corner#
Intent.ACTION_VIEWpredates the smartphone as we know it. It was designed for a device with a physical keyboard and no touchscreen.- A chooser is an activity. The system's disambiguation UI is itself a component, which is why it can be replaced by an OEM.
CATEGORY_DEFAULTexists because filters are opt-in. Without it, every implicit intent would match nothing, since filters would have no way to say "I am open to ordinary requests".
Try it#
adb shell am start -a android.intent.action.VIEW -d https://zygote.example/
Example output
Starting: Intent { act=android.intent.action.VIEW dat=https://zygote.example/ }
Priority: 0 Preferred: 0
...adb shell cmd package query-activities -a android.intent.action.SEND -t text/plain
Check yourself#
Check yourself
You call `startActivity()` for an activity in another app. Which check does the system perform before delivering the intent?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsPackage visibility filtering on Androiddeveloper.android.comaccessed 2026-10-27↩
- docsApp Linksdeveloper.android.comaccessed 2026-10-27↩
- docsFeatures and APIs overview | Android 12developer.android.comaccessed 2026-10-27↩
- docsBehavior changes: Apps targeting Android 13 or higherdeveloper.android.comaccessed 2026-10-27↩
- docsBehavior changes: Apps targeting Android 16 or higherdeveloper.android.comaccessed 2026-10-27↩
- docsIntents and intent filters | App architecturedeveloper.android.comaccessed 2026-10-27↩
- docsApp manifest overviewdeveloper.android.comaccessed 2026-10-27↩