What Android Actually Dispatches

Android’s NFC Reader/Writer path is easiest to judge once the phrase “a tag was scanned” is broken into concrete system steps. This chapter traces what Android creates after discovery, how it chooses an Activity, and which checks govern whether another app can start that Activity. The practical goal is to distinguish a routing decision from proof that the request came from NFC hardware.

From a discovered tag to an application-facing object

The normal path begins with a real near-field event. A tag comes within range, the NFC controller detects it, and Android receives information through the device’s NFC stack and driver. That hardware step matters because it is what gives a tag interaction its physical meaning. The phone is not merely receiving arbitrary application data; it is responding to a discovery performed by the NFC subsystem.

Android represents the discovered object as a Tag. The official API describes this as an immutable object representing the state of the tag at the time it was discovered. It can serve as a handle for technology-specific operations, and an application can query its identifier with getId() or the technologies it supports with getTechList(). Android creates a new tag object for each discovery, even if the same physical object comes into range again. A prior object is not simply a permanent ticket that proves every later interaction with that tag.

The identifier itself has limited meaning. Many tags have a stable unique identifier, but some generate a random identifier for each discovery and some have no identifier at all. The size and format depend on the radio technology. This is useful context for developers: a tag ID may help identify or interpret a tag, but it is not a universal cryptographic identity, and it is not automatically an origin credential for the Activity that receives it.

Android passes the Tag object to a single Activity through the NfcAdapter.EXTRA_TAG extra in an Intent. That sentence describes a transition between two worlds. Inside the NFC stack, the object is associated with a discovery event. At the application boundary, the event is represented as an Intent, an object that carries an action, categories, data, and extras and is delivered through Android’s Activity-start machinery. The receiving Activity can read the values it needs, but it receives them as application-facing input.

Android chooses the Activity through a sequence designed for transient interactions. A user may have only a moment to keep the phone near a tag, so the system tries to select a suitable handler without forcing an unnecessary app-choice dialog. The first stage gives priority to a foreground Activity that has explicitly registered for foreground dispatch. That is useful when the user is already inside an app that wants to handle the next tag.

If foreground dispatch does not settle the event, Android examines NDEF data. NDEF is a common format for records stored on tags. When the first relevant record is a URI, a Smart Poster, or MIME data, Android uses the ACTION_NDEF_DISCOVERED action. For a URI or Smart Poster, the URI becomes the Intent’s data; for MIME data, the MIME type becomes the Intent’s type. This lets an Activity declare that it wants to handle, for example, a particular kind of content rather than every tag in range.

If no matching NDEF handler is found, Android can use technology dispatch. With ACTION_TECH_DISCOVERED, the system looks for an Activity that handles one or more technologies present on the tag. If that stage also finds no match, the fallback is ACTION_TAG_DISCOVERED. The four stages—foreground activity, NDEF data, technology, and fallback—are not four different kinds of physical tap. They are four increasingly broad ways to route the same discovery to an Activity.

The ordering reflects a usability trade-off. NDEF matching is preferred because it can describe content across different tag technologies: an Activity can ask for a kind of URI or MIME record without tying itself to one hardware implementation. Technology dispatch is more specific to the tag’s capabilities, while the fallback is deliberately broad. If several Activities match at a stage, Android may present a chooser so the user can select a recipient. If one match is sufficient, the system completes dispatch. In both cases, the resolver is answering a selection question. It is not making the application prove that the selected Activity’s interpretation of the data is safe.

This also explains why the same tag can be treated differently by different applications or on different device states. A foreground Activity can take priority. A tag with a particular NDEF record can select a narrower handler than a tag with no NDEF data. A technology match can be reached only after earlier stages fail. These are predictable consequences of the dispatch design, not evidence that one handler is more physically authentic than another. The physical discovery is the common starting point; the software route depends on what the system and installed applications declare they can handle.

sequenceDiagram participant T as NFC tag participant H as NFC hardware and driver participant N as Android NFC stack participant R as Intent resolver participant A as Target Activity T->>H: Enters near-field range H->>N: Report discovery and tag state N->>N: Create Tag object and discovery Intent N->>R: Resolve action, data, or technology match R->>R: Apply export and caller-permission checks R->>A: Start handler with tag or NDEF-related data A->>A: Parse input and enter app workflow

This sequence shows why the API feels coherent to a user. Hardware discovery, object creation, resolution, and application behavior happen quickly, and the last step is usually the visible one. But it also shows where different kinds of authority meet. The NFC stack knows that it observed a discovery. The resolver knows how to match a request to a component. The Activity receives the result and decides what to do. Those are related responsibilities, not identical guarantees.

From Intent matching to cross-app startability

The resolver selects a handler by comparing the discovery Intent with manifest-declared intent filters. A filter can specify one of the NFC discovery actions, a category such as the default category used in ordinary Activity resolution, and additional data constraints. For NDEF dispatch, those constraints can include a URI pattern or MIME type. For technology dispatch, they can describe the NFC technologies a handler accepts.

The important point is that these fields are predicates for matching. They answer a question like, “Does this component say that it handles a request with this action and data?” They do not inherently answer, “Was this request produced by the NFC system after a physical discovery?” An action can be meaningful to the resolver without being a proof of the sender’s identity.

After a component is selected, Android applies the ordinary rules for starting an Activity across package boundaries. One important rule concerns whether the Activity is exported. An exported component is available for access from outside its own application, subject to the other platform checks. A non-exported handler is not generally available to an unrelated app through the normal cross-app start path. For Android 12 and later, an Activity that declares an intent filter must explicitly state its exported status, making this part of the manifest more visible as a developer decision. Earlier versions have different defaults and legacy interpretations, so the effective behavior remains version-sensitive.

A second rule concerns android:permission. A handler can require the caller to hold a particular permission before Android allows the start. That permission gate is separate from whether the Intent matches. A request may have exactly the right action and data and still fail because the caller is not authorized to start the selected component.

These checks are real boundaries. The existence of a generic Intent path does not mean that every handler is publicly reachable or that every Activity launch will succeed. A component can be private, permission-protected, missing in the installed package, or rejected by a runtime check. The paper’s security finding is therefore not “any application can start any NFC Activity.” It concerns the subset of RW handlers whose manifest exposure and runtime conditions allow a third-party app to reach them.

It helps to treat the checks as a sequence rather than as one vague security setting. First, the resolver has to find a filter whose action and data constraints accept the request. Then Android has to decide which matching component will receive it. Finally, the component-start policy has to permit the caller to cross the package boundary. A manifest can fail at any of these points. A filter may be too narrow, no handler may be installed, a component may not be exported, or a required caller permission may be missing. Even a component that looks exposed in static metadata can encounter a runtime condition that blocks delivery. The paper’s later measurement therefore distinguishes manifest candidates from on-device confirmation instead of treating the first match as the final result.

The distinction between export and permission is also important for design intent. An exported handler says, in effect, that cross-package access is part of the component’s permitted operating model, while a caller permission narrows which packages may use that access. Neither setting changes the meaning of the action or data fields. They govern who may start the component under the platform’s generic rules; they do not turn a URI, MIME type, or NFC action into a signed statement from the NFC subsystem.

This is also why android.permission.NFC needs careful interpretation. The paper distinguishes permission to access NFC APIs from authorization to receive a tag-dispatch delivery at a manifest-registered handler. A local app that does not use NFC hardware may still be able to attempt an ordinary cross-app Activity start if the target component is externally startable. Conversely, possessing NFC-related capability does not transform an ordinary Intent into authenticated proof of physical origin. Hardware access and delivery authorization are different control planes.

The normal flow can therefore be described at two levels. At the hardware level, Android has a reason to construct the discovery request: it observed a tag. At the routing level, Android asks which Activity matches the request and whether standard component-start rules allow the transition. The first level explains why the event began. The second level determines where the software request goes. In ordinary use they are joined by the system, but the matching fields exposed to the app primarily express the second level.

That separation matters when a developer designs the Activity’s first decisions. Suppose an Activity receives ACTION_NDEF_DISCOVERED with a matching URI or MIME type. The filter has done its job: it helped Android select the Activity. Suppose the Activity then parses an NDEF-derived value and begins provisioning a device. That parsing step can validate the content’s shape or syntax. It still does not, by itself, verify that the NFC stack produced the request after a fresh physical discovery. The app may know what was delivered without knowing who was entitled to cause the delivery.

A developer can reasonably treat a system-managed event as trustworthy only when the platform exposes a guarantee that the application can rely on. The official Tag reference documents what a normal discovery produces and how Android dispatches it. It does not say that every Activity can independently authenticate the origin of the Intent it receives. The security analysis builds on that difference: documented routing behavior should not be silently upgraded into a provenance guarantee that the receiving app cannot verify.

There is a useful operational distinction here. A filter is a declaration of interest. Export status and caller permissions are access controls. A Tag object and NDEF fields are delivered data. None of those roles should be confused with a system-only statement that “this Activity launch was admitted because the NFC dispatcher observed a tag.” If the platform does not enforce that last condition at the package boundary, an application that treats handler entry as proof of proximity is depending on an assumption outside the ordinary meaning of the fields it receives.

This does not make the normal Android design irrational. Generic routing is flexible, familiar to developers, and efficient for a transient interaction. It lets many apps participate in NFC without a bespoke handoff protocol for each one. The trade-off is that a low-friction routing interface can become security-sensitive when downstream code interprets its arrival as authorization. The system can be excellent at finding a receiver while still leaving the receiver without a dependable answer about the request’s origin.

The next question follows directly from that boundary. If Android uses ordinary implicit-Intent resolution and ordinary Activity-start checks to deliver RW discovery, then an app that sees only the resulting fields may be unable to tell a hardware-created request from a software-created one. The issue is no longer how Android finds a handler. It is what the handler is allowed to infer from being found.

References