Mobile Computing · Mobile Platforms
Intent Resolution, Exported Components, and URI Grants
How a late-bound call is matched to a component, what makes a component reachable from other apps, and why a URI permission is a separate grant from the intent that carries it.
An intent is a message that names what should happen rather than which code should handle it. The indirection is what makes mobile platforms extensible: a camera app can be installed after the app that wants to take a photo, and the request still resolves. It is also what makes the security model subtle, because a call whose target is decided at runtime by a system service has no compile-time guarantee about who receives it.
Resolution is a match against declared filters
A component becomes a candidate for an intent if its manifest declares a filter that matches the intent’s action, category, and data. Matching is not a string comparison against a registry of names; it is a structured comparison performed by the package manager.
The action must match exactly. Categories are a subset test: every category in the intent must appear in the filter, but the filter may declare categories the intent did not mention. Data is matched by scheme, host, port, path, and MIME type, in that order of specificity, with wildcards permitted at the host and path levels and in the MIME type’s subtype.
Two matching rules cause most of the confusion. An intent with no data specification matches any filter whose action and categories match, regardless of the filter’s data element, which means a filter that intends to restrict itself to a scheme can still be reached by a caller that omits the data. And an implicit intent with the BROWSABLE category is reachable from a web browser, which means the filter’s declared host patterns determine what a web page can invoke.
<activity android:name=".ShareTarget" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
The filter above is reachable by any app that sends an image, and by nothing else: the MIME type narrows the match, and the absence of the BROWSABLE category keeps it out of reach of a web browser. Removing the mimeType line would widen it to every SEND intent on the device, which is the difference between a narrow exported surface and a general one.
The result of resolution is a ranked list, not a single component. Ranking considers whether the filter is an exact match, the declared priority, and whether the component is the caller’s own. The service then either picks the highest-ranked candidate, shows a chooser if several are equally eligible and the intent is implicit, or fails with an activity-not-found error. A caller that does not check for the failure crashes with an exception that names the intent, which is the correct behaviour but a common source of defects when a device lacks the expected handler.
Exported is a property of the filter, not of the class
A component is reachable from other apps if it declares an intent filter, or if it explicitly sets the exported attribute. On current platform versions the attribute must be set explicitly whenever a filter is present, and the installer rejects a manifest that omits it. The requirement was introduced because the default was previously derived from the presence of a filter, and a component that was intended for internal use became reachable by accident when a filter was added for a related purpose.
Reachability is not authorisation. An exported component can be started by any app, subject to the permission checks it declares, which is the same grant database described in Permission Grants and Where They Are Enforced. A component that declares a permission in its manifest requires the caller to hold that permission, and the check is performed by the activity manager when the launch is requested, using the caller’s identity as supplied by the binder driver rather than anything in the intent.
The permission check has a hole that is easy to open. A component that requires a permission which no ordinary app holds is effectively private, but a component that requires no permission is public, and a public component that trusts its input is a vulnerability. The classic case is an exported activity that reads an extra from the intent and uses it as a file path or a URI, which lets any app make the component perform an operation with its own privileges. The platform’s mitigation is that a URI in an intent extra is not accessible to the receiver unless a grant accompanies it, described below, which removes the most common form of the attack.
Grants travel with the data, not with the message
A content URI names a row in a provider. Naming it is not permission to read it: the provider checks the caller’s access when the request arrives, using the caller’s UID against its own tables. An app that receives a URI from another app therefore cannot open it, even though it can see the string.
The mechanism that makes the URI usable is a grant recorded against the receiving package for that specific URI, with a mode of read, write, or both, and a lifetime tied to the receiving activity or to an explicit revoke. The sending app requests the grant when it starts the activity or when it returns a result, and the activity manager records it and passes it to the provider, which consults it in addition to its own checks.
Three properties follow, and they explain most of the observed behaviour.
The grant is for the receiving package, not for the intent. An app that receives a granted URI and forwards it in a new intent does not transfer the grant unless it sets the flag that propagates it, and even then the propagation is one hop: the second receiver gets a grant of its own, derived from the first, and the chain terminates at the activity that received it.
The grant can be narrower than the provider. A provider can grant a prefix grant covering a subtree of its URI space, which is how a document tree is shared: the receiver can enumerate and read any document under the granted root, without the sender naming each one.
The grant is revocable and is revoked automatically when the receiving component finishes. A URI cached beyond that point fails with a security exception, and the failure is indistinguishable from a genuine permission denial, which is why long-lived access requires a persisted grant requested explicitly.
Why the indirection is worth the cost
The alternative to intents is a registry of interfaces: an app that wants a photo asks a known camera service, and the service is fixed. That design has better security properties, because the caller and callee are known at compile time and the interface is narrow. It has worse extensibility, because replacing the camera means changing every caller, and it has worse portability across vendors, because each vendor’s camera would need its own interface.
The intent model moves the decision to runtime and pays for it in three currencies. Resolution cost: a match over all installed filters, cached by the package manager and invalidated on install, which is why the first resolution after an install is slower than subsequent ones. Ambiguity: several components may match, and the platform’s answer is either a chooser or a priority rule, both of which can surprise the caller. Attack surface: every exported component is reachable, and the set of exported components is a property of the installed apps, which changes after the caller ships.
The mitigation for the third is explicit: components that do not need to be reachable are declared not exported, filters are declared only where a third party genuinely needs to invoke them, and the data received is validated as untrusted input. The platform cannot enforce any of this, because whether a component should be public is a design decision only the app’s author can make.
The result channel and its ordering
Starting a component for a result is a separate operation from starting it, with its own lifetime. The caller supplies an identifier, the service records the caller’s process and the identifier, and when the target finishes it returns a result that the service routes back to the caller.
The routing is by identity, not by handle: the result arrives as a message to the calling process, and if that process has died the result is discarded. This is why a result cannot be delivered to a process that was reclaimed while the camera app was in the foreground, and why the caller must treat the absence of a result as a possible outcome rather than as an error. The reclaim itself is decided by the policy in Memory Pressure, Reclaim Order, and the Kill Decision, which has no knowledge of a pending result.
The result is delivered before the caller’s activity is resumed, which means the caller’s state at the time of delivery is the state it was in when it started the target, not the state after any intervening lifecycle transitions. Code that assumes otherwise will read a stale field.
How resolution fails in practice
An implicit intent that matches nothing throws, and the exception is thrown from the call site on the caller’s main thread. Checking resolvability before starting is a separate query that can return a positive answer and still fail, because the resolution can change between the query and the start when an app is uninstalled concurrently.
A filter that declares a scheme and a host but no path matches every path on that host. A filter that declares a MIME type of a top-level type with a wildcard subtype matches every type under it, which is broader than most authors intend.
An exported component that declares a permission whose protection level is normal is reachable by any app that declares the same permission, which is any app at all if the permission is declared by the platform rather than by the component’s own package. Declaring a custom permission with a signature protection level is the only way to restrict reachability to a known set of signers.
Finally, a task that is launched into another app’s task stack inherits that task’s affinity. An activity started with the flag that places it in the caller’s task will be destroyed and recreated with the caller’s configuration, and its own declared launch mode is overridden. The interaction between task affinity and launch flags is the part of the model with the most version-dependent behaviour, and it is the reason component contracts are easier to reason about when each component is exported with an explicit, narrow interface rather than relying on the default task behaviour.
References
- Android Developers — Intents and intent filters — Android Developers
- Android Developers — Content providers and URI permissions — Android Developers
- Android Open Source Project — Core platform architecture — AOSP documentation