NFC behavior changes in Android 17
In May 2026, I realized that all of a sudden, NFC check-ins for Damier no longer seemed to function correctly. At first, I assumed it was just a writing issue, since tapping a chip with Damier being in writing mode would not update the ID, but that was wrong. The chip was being written correctly. I simply didn't verify it and assumed the writing was failing because reads were broken.
The setup
Damier used Cue, a small NFC library I wrote specifically for it. Tags are written in the format cue://damier/<uuid>
using NdefRecord.createUri(), producing a TNF_WELL_KNOWN + RTD_URI record.
Reading goes through Android's standard NDEF intent dispatch system: the OS scans the tag, matches it against the app's
intent filter, and fires ACTION_NDEF_DISCOVERED into onNewIntent().
Or at least, that's how it's supposed to work.
Debugging
The first thing I checked was whether onNewIntent() was even firing. It wasn't. Android wasn't dispatching the NFC
intent to Damier at all, which immediately ruled out any bug in Cue's parsing logic, since that code only runs after
the intent arrives.
I checked everything I could think of:
- the
NDEF_DISCOVEREDintent filter was intact, with the correct scheme and host within the Merged Manifest. - I checked every app with NFC permission using Exodus to check if other apps are intercepting the tag
- Damier uses
enableReaderModefor writing chips. If reader mode stays active, it intercepts tags before the system dispatch can fire. I confirmed the write state was correctly returning toIdleanddisableReaderModewas being called. - NFC Tools confirmed the tag contained exactly
cue://damier/<uuid> isTagIntentAllowed()returnedtrue. Android 16 introduced a new NFC tag scanning allowlist under Settings → Apps → Special app access → Launch via NFC. Damier was allowed.- I tested whether Android 16 was routing
cue://asACTION_VIEW(documented forhttp://andhttps://schemes). It wasn't,ACTION_VIEWdidn't fire either. - I temporarily rewrote a tag with MIME type
application/de.lijucay.damierusing NFC Tools and dded the corresponding filter. Still no dispatch. - I stripped the filter down to just
NDEF_DISCOVEREDwith no<data>at all. Still nothing. - I built a completely empty app with a single Activity, a single NFC filter, and one log line in
onNewIntent(). Tapping the tag producedaction=android.intent.action.MAIN, meaning Android was launching the app but discarding the NFC intent entirely.
At this point I was fairly convinced it was a Pixel 10 Pro device bug. So at 3 am, I reflashed the operating system.
After the flash
Immediately after the flash, before any updates, NFC worked perfectly. I tapped a chip, Damier opened, check-in logged. The next day, I finished setting up the phone and installed the pending Google Play System Update. A few hours later, I tapped a chip, and it didn't work again. The only thing that changed this time was the Google Play System update.
This time I tried something I had dismissed too early: I set targetSdk back to 36. NFC immediately started working
again. Bumping it back to 37 broke it again.
The actual cause
The Google Play System Update had backported a behavior change from Android 17 (API level 37): apps targeting SDK 37 now
require the android.permission.DISPATCH_NFC_MESSAGE permission on any Activity that receives NFC intents.
This is documented in NFC Basics, but it was not listed on Behavior changes: Apps targeting Android 17 or higher, which is the page developers actually check when bumping their target SDK. It was quietly added to a separate documentation page. Two days of debugging for an undocumented breaking change.
From the docs:
Starting Android 17 (API level 37), for an activity to be dispatched an NFC intent, if the app targets SDK > Build.VERSION_CODES.BAKLAVA, it must be protected by the android.permission.DISPATCH_NFC_MESSAGE permission. This ensures that only the NFC system service can dispatch intents to your activity. Additionally, the system will not dispatch NFC intents to applications that are in a stopped state (e.g. if the application has never been launched by the user or has been force-stopped).
The reason you can't just add this permission to MainActivity directly is that android:permission on an Activity
restricts who can start it. With DISPATCH_NFC_MESSAGE set, only the NFC system service can launch that Activity,
meaning tapping the app icon would produce "App not installed". The launcher doesn't hold that permission, so it can't
start the Activity at all.
The fix
The solution is a dedicated NfcDispatchActivity that holds the permission, receives the NFC intent, forwards it to
MainActivity, and immediately finishes itself.
In AndroidManifest.xml:
<activity
android:name=".nfc.NfcDispatchActivity"
android:exported="true"
android:permission="android.permission.DISPATCH_NFC_MESSAGE">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="cue" android:host="@string/host"/>
</intent-filter>
</activity>
And the Activity itself:
class NfcDispatchActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val nfcIntent = intent
startActivity(
Intent(this, MainActivity::class.java).apply {
action = nfcIntent.action
data = nfcIntent.data
putExtras(nfcIntent)
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
)
finish()
}
}
MainActivity receives the forwarded intent in onNewIntent() as normal, and nothing else in the app needs to change.
It's not elegant, but it works, and it keeps all existing functionality intact. If Google had put this in their behavior changes list, it would have been a 10-minute fix instead of a two-day investigation.
Summary
If your app targets SDK 37 and NFC dispatch suddenly stopped working after a Google Play System Update — even though
your manifest, filters, and tag contents are all correct — this is almost certainly the cause. Add a dedicated
NfcDispatchActivity with android:permission="android.permission.DISPATCH_NFC_MESSAGE" and forward the intent to your
main Activity.
And maybe file some feedback with Google about documenting breaking changes where developers will actually find them.