From 6c8793546a5c60df213098c672a543c57e005d75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 12:04:09 +0000 Subject: [PATCH 1/3] fix(nfc): remove enableReaderMode that blocks HCE phone-to-phone sharing --- .../openbeam/sharing/NfcShareHelper.kt | 42 ++++--------------- .../metadata/android/en-US/changelogs/1.txt | 2 +- 2 files changed, 10 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/net/duhowpi/openbeam/sharing/NfcShareHelper.kt b/app/src/main/java/net/duhowpi/openbeam/sharing/NfcShareHelper.kt index 6587b29..664fa1e 100644 --- a/app/src/main/java/net/duhowpi/openbeam/sharing/NfcShareHelper.kt +++ b/app/src/main/java/net/duhowpi/openbeam/sharing/NfcShareHelper.kt @@ -16,14 +16,13 @@ import android.nfc.NfcAdapter * NFC-capable reader — another Android device, iPhone (iOS 13+), or dedicated hardware — * to tap and read the NDEF content. No physical NFC tag is written. * - * While sharing is active, [startSharing] calls [NfcAdapter.enableReaderMode] with all four - * NFC technology flags (NFC_A, NFC_B, NFC_F, NFC_V) plus [NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK] - * and [NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS]. This puts the NFC stack into reader mode for - * every tag type, routing any physical tag discovery to our no-op callback instead of the OS - * NDEF/TAG intent dispatch, so the device will not inadvertently open URLs or launch apps when a - * physical NFC tag (Mifare, NTAG2xx, ISO-DEP, etc.) is brought near it. HCE card emulation runs - * on an independent path in the NFC controller and remains active throughout. [stopSharing] calls - * [NfcAdapter.disableReaderMode] to restore normal tag-dispatch behaviour when sharing ends. + * HCE operates in NFC card-emulation (listening) mode: the NFC controller waits for an + * external reader to initiate contact and issue ISO 7816-4 APDU commands. Calling + * [NfcAdapter.enableReaderMode] would switch the NFC controller into active-polling mode, + * which generates its own RF field and prevents HCE from responding — especially when tapping + * two Android phones together (RF collision). Therefore [startSharing] deliberately does NOT + * call [NfcAdapter.enableReaderMode], leaving the controller in its default mode where card + * emulation is fully active. * * [NdefHceService] is bound by the NFC subsystem when a reader selects the NDEF Application * AID; [startSharing] / [stopSharing] gate the content and callbacks via a single atomic @@ -43,8 +42,7 @@ class NfcShareHelper(private val activity: Activity) { activity.packageManager.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION) /** - * Register [message] for HCE emulation so the next reader tap can receive it, and suppress - * physical-tag dispatch so the device behaves purely as a card emulator. + * Register [message] for HCE emulation so the next reader tap can receive it. * Should be called from Activity.onResume (paired with [stopSharing] in onPause). * * @param message NDEF message to emit when another device taps. @@ -63,33 +61,11 @@ class NfcShareHelper(private val activity: Activity) { onComplete = { onResult(NfcShareState.Success) }, ) - // Suppress physical-tag dispatch while HCE is active. - // enableReaderMode with ALL technology flags (NFC_A/B/F/V) puts the NFC stack into - // reader mode for every tag type. Any physical tag (Mifare, NTAG2xx, ISO-DEP, FeliCa, - // ISO 15693) discovered during sharing is routed to our no-op callback instead of being - // dispatched via NDEF_DISCOVERED / TAG_DISCOVERED intents, so the OS will not open URLs - // or launch other apps. FLAG_READER_SKIP_NDEF_CHECK skips the slow NDEF-compatibility - // check on discovered tags (we ignore them anyway). FLAG_READER_NO_PLATFORM_SOUNDS - // suppresses the NFC discovery sound. HCE card emulation runs on an independent path - // in the NFC controller and is unaffected by reader mode. - adapter?.enableReaderMode( - activity, - { /* physical tag discovered during sharing – intentionally ignored */ }, - NfcAdapter.FLAG_READER_NFC_A or - NfcAdapter.FLAG_READER_NFC_B or - NfcAdapter.FLAG_READER_NFC_F or - NfcAdapter.FLAG_READER_NFC_V or - NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK or - NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS, - null, - ) - onResult(NfcShareState.Waiting) } - /** Restore normal NFC tag polling and clear HCE content. Call from Activity.onPause. */ + /** Clear HCE content. Call from Activity.onPause. */ fun stopSharing() { - adapter?.disableReaderMode(activity) // Clear atomically so the service never sees a session with null callbacks. NdefHceService.session = null } diff --git a/fastlane/metadata/android/en-US/changelogs/1.txt b/fastlane/metadata/android/en-US/changelogs/1.txt index e911a44..283f6cb 100644 --- a/fastlane/metadata/android/en-US/changelogs/1.txt +++ b/fastlane/metadata/android/en-US/changelogs/1.txt @@ -8,4 +8,4 @@ Initial release. Fixes: - Removed duplicate app title shown outside the share dialog - NFC sharing no longer requires a physical tag — just tap devices together -- Device no longer reads physical NFC tags while sharing is active \ No newline at end of file +- Fixed NFC sharing between two Android phones not working \ No newline at end of file From b17a71f345b2488c0aed213dc083e0064e2632e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 12:19:15 +0000 Subject: [PATCH 2/3] fix(nfc): use foreground dispatch to suppress tag reading while HCE is active --- .../net/duhowpi/openbeam/ShareActivity.kt | 6 +- .../openbeam/sharing/NfcShareHelper.kt | 55 ++++++++++++++++--- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/net/duhowpi/openbeam/ShareActivity.kt b/app/src/main/java/net/duhowpi/openbeam/ShareActivity.kt index c5aec25..7a98fda 100644 --- a/app/src/main/java/net/duhowpi/openbeam/ShareActivity.kt +++ b/app/src/main/java/net/duhowpi/openbeam/ShareActivity.kt @@ -131,8 +131,10 @@ class ShareActivity : AppCompatActivity() { /** Forward any remaining share intents received while activity is on top (singleTop). */ override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - // NFC NDEF sharing is handled by NdefHceService (HCE); no NFC intents are dispatched - // to the activity. Only new ACTION_SEND intents can arrive here. + // NFC tag intents (ACTION_NDEF_DISCOVERED, ACTION_TAG_DISCOVERED) arrive here via + // foreground dispatch while HCE sharing is active. They are intentionally ignored: + // this device is the card emulator, not the reader. + // Only new ACTION_SEND intents would need further handling. } override fun onRequestPermissionsResult( diff --git a/app/src/main/java/net/duhowpi/openbeam/sharing/NfcShareHelper.kt b/app/src/main/java/net/duhowpi/openbeam/sharing/NfcShareHelper.kt index 664fa1e..43a5d6c 100644 --- a/app/src/main/java/net/duhowpi/openbeam/sharing/NfcShareHelper.kt +++ b/app/src/main/java/net/duhowpi/openbeam/sharing/NfcShareHelper.kt @@ -1,9 +1,12 @@ package net.duhowpi.openbeam.sharing import android.app.Activity +import android.app.PendingIntent +import android.content.Intent import android.content.pm.PackageManager import android.nfc.NdefMessage import android.nfc.NfcAdapter +import android.os.Build /** * Manages NFC NDEF sharing via Host Card Emulation (HCE). @@ -16,13 +19,20 @@ import android.nfc.NfcAdapter * NFC-capable reader — another Android device, iPhone (iOS 13+), or dedicated hardware — * to tap and read the NDEF content. No physical NFC tag is written. * - * HCE operates in NFC card-emulation (listening) mode: the NFC controller waits for an - * external reader to initiate contact and issue ISO 7816-4 APDU commands. Calling - * [NfcAdapter.enableReaderMode] would switch the NFC controller into active-polling mode, - * which generates its own RF field and prevents HCE from responding — especially when tapping - * two Android phones together (RF collision). Therefore [startSharing] deliberately does NOT - * call [NfcAdapter.enableReaderMode], leaving the controller in its default mode where card - * emulation is fully active. + * **Preventing "read instead of share" on Android** + * + * By default, when the Android NFC stack detects a nearby tag or HCE card it dispatches + * ACTION_NDEF_DISCOVERED (or ACTION_TAG_DISCOVERED) to apps on this device. Without any + * suppression, this causes the sharing phone to simultaneously act as a reader — opening + * browsers, contacts, etc. — when it taps another phone. + * + * [NfcAdapter.enableReaderMode] can suppress this dispatch, but it may also disable card + * emulation on some chipsets/firmware, breaking HCE in the phone-to-phone direction. + * + * The correct solution is [NfcAdapter.enableForegroundDispatch]: it routes all incoming NFC + * tag intents to our activity's onNewIntent (where they are silently ignored) instead of + * the OS dispatcher, without touching the card-emulation path at all. HCE therefore remains + * fully active while the polling side-effect of reading is suppressed at the application layer. * * [NdefHceService] is bound by the NFC subsystem when a reader selects the NDEF Application * AID; [startSharing] / [stopSharing] gate the content and callbacks via a single atomic @@ -42,7 +52,9 @@ class NfcShareHelper(private val activity: Activity) { activity.packageManager.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION) /** - * Register [message] for HCE emulation so the next reader tap can receive it. + * Register [message] for HCE emulation so the next reader tap can receive it, and enable + * foreground dispatch so that any NFC tag discovered by this device is routed to our + * activity (where it is ignored) rather than dispatched OS-wide. * Should be called from Activity.onResume (paired with [stopSharing] in onPause). * * @param message NDEF message to emit when another device taps. @@ -61,14 +73,39 @@ class NfcShareHelper(private val activity: Activity) { onComplete = { onResult(NfcShareState.Success) }, ) + // Route all incoming NFC tag/NDEF intents to our activity instead of letting the OS + // dispatch them to browsers or other apps. Our activity's onNewIntent ignores these + // intents, so the sharing phone never acts as a reader. Unlike enableReaderMode, + // foreground dispatch does not affect the card-emulation path, so HCE remains active. + adapter?.enableForegroundDispatch(activity, buildNfcPendingIntent(), null, null) + onResult(NfcShareState.Waiting) } - /** Clear HCE content. Call from Activity.onPause. */ + /** Restore normal NFC dispatch and clear HCE content. Call from Activity.onPause. */ fun stopSharing() { + adapter?.disableForegroundDispatch(activity) // Clear atomically so the service never sees a session with null callbacks. NdefHceService.session = null } + + // ------------------------------------------------------------------------- + + /** + * Build a PendingIntent that brings our activity to the foreground when the NFC + * subsystem fires a tag-discovered intent via foreground dispatch. + * FLAG_MUTABLE is required on API 31+ so the system can add TAG/NDEF extras to the intent. + */ + private fun buildNfcPendingIntent(): PendingIntent { + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + val intent = Intent(activity, activity::class.java) + .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + return PendingIntent.getActivity(activity, 0, intent, flags) + } } /** States emitted during an NFC share operation. */ From b64c0b3f6a5475dfeafcb0559300fd846ba5265f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Jul 2026 12:36:00 +0000 Subject: [PATCH 3/3] fix(nfc): pass explicit IntentFilters to enableForegroundDispatch to suppress NDEF chooser --- .../openbeam/sharing/NfcShareHelper.kt | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/net/duhowpi/openbeam/sharing/NfcShareHelper.kt b/app/src/main/java/net/duhowpi/openbeam/sharing/NfcShareHelper.kt index 43a5d6c..5910451 100644 --- a/app/src/main/java/net/duhowpi/openbeam/sharing/NfcShareHelper.kt +++ b/app/src/main/java/net/duhowpi/openbeam/sharing/NfcShareHelper.kt @@ -3,6 +3,7 @@ package net.duhowpi.openbeam.sharing import android.app.Activity import android.app.PendingIntent import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageManager import android.nfc.NdefMessage import android.nfc.NfcAdapter @@ -19,20 +20,28 @@ import android.os.Build * NFC-capable reader — another Android device, iPhone (iOS 13+), or dedicated hardware — * to tap and read the NDEF content. No physical NFC tag is written. * - * **Preventing "read instead of share" on Android** + * **Preventing the OS tag-dispatch chooser while sharing** * - * By default, when the Android NFC stack detects a nearby tag or HCE card it dispatches - * ACTION_NDEF_DISCOVERED (or ACTION_TAG_DISCOVERED) to apps on this device. Without any - * suppression, this causes the sharing phone to simultaneously act as a reader — opening - * browsers, contacts, etc. — when it taps another phone. + * Android resolves NFC tag discoveries in three tiers: + * 1. ACTION_NDEF_DISCOVERED — highest priority; MIME/URI filtering. + * 2. ACTION_TECH_DISCOVERED — technology-list matching. + * 3. ACTION_TAG_DISCOVERED — catch-all fallback. * - * [NfcAdapter.enableReaderMode] can suppress this dispatch, but it may also disable card - * emulation on some chipsets/firmware, breaking HCE in the phone-to-phone direction. + * [NfcAdapter.enableForegroundDispatch] intercepts these intents before they reach normal + * app dispatch, routing them to our activity's onNewIntent (where they are ignored) instead. + * Passing `null, null` for the filters/techLists parameters only hooks into the + * ACTION_TAG_DISCOVERED tier. When the other device's NFC carries NDEF data, Android + * evaluates ACTION_NDEF_DISCOVERED first — and if that tier is not intercepted by our + * foreground dispatch, the OS shows an app-chooser dialog for every installed app that + * handles that NDEF type. * - * The correct solution is [NfcAdapter.enableForegroundDispatch]: it routes all incoming NFC - * tag intents to our activity's onNewIntent (where they are silently ignored) instead of - * the OS dispatcher, without touching the card-emulation path at all. HCE therefore remains - * fully active while the polling side-effect of reading is suppressed at the application layer. + * The fix is to supply explicit [IntentFilter] arrays that cover all three tiers, + * including ACTION_NDEF_DISCOVERED with a wildcard `*∕*` MIME type so every NDEF tag + * is captured regardless of content type. + * + * Note: [NfcAdapter.enableReaderMode] is intentionally NOT used here. Per the Android + * documentation it disables HCE/card-emulation routing for the duration it is active, + * which would prevent the receiving device from reading our emulated tag. * * [NdefHceService] is bound by the NFC subsystem when a reader selects the NDEF Application * AID; [startSharing] / [stopSharing] gate the content and callbacks via a single atomic @@ -73,11 +82,16 @@ class NfcShareHelper(private val activity: Activity) { onComplete = { onResult(NfcShareState.Success) }, ) - // Route all incoming NFC tag/NDEF intents to our activity instead of letting the OS - // dispatch them to browsers or other apps. Our activity's onNewIntent ignores these - // intents, so the sharing phone never acts as a reader. Unlike enableReaderMode, - // foreground dispatch does not affect the card-emulation path, so HCE remains active. - adapter?.enableForegroundDispatch(activity, buildNfcPendingIntent(), null, null) + // Intercept all three NFC dispatch tiers so the OS never shows a tag-chooser dialog + // or launches other apps while sharing is active. Explicit filters are required: + // passing null covers only ACTION_TAG_DISCOVERED, leaving ACTION_NDEF_DISCOVERED free + // to trigger the system chooser when the other device carries NDEF data. + adapter?.enableForegroundDispatch( + activity, + buildNfcPendingIntent(), + buildDispatchFilters(), + null, + ) onResult(NfcShareState.Waiting) } @@ -106,6 +120,26 @@ class NfcShareHelper(private val activity: Activity) { .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) return PendingIntent.getActivity(activity, 0, intent, flags) } + + /** + * Build IntentFilters that intercept all three NFC dispatch tiers: + * - ACTION_NDEF_DISCOVERED with `*∕*` to catch NDEF tags of any MIME type or URI. + * - ACTION_TECH_DISCOVERED as a fallback for non-NDEF tech-routed tags. + * - ACTION_TAG_DISCOVERED as the final catch-all. + * + * All matching intents are delivered to our activity's onNewIntent and ignored there, + * preventing the OS from routing them to other apps or showing a chooser dialog. + */ + private fun buildDispatchFilters(): Array { + val ndefFilter = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED).apply { + try { addDataType("*/*") } catch (_: IntentFilter.MalformedMimeTypeException) {} + } + return arrayOf( + ndefFilter, + IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED), + IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED), + ) + } } /** States emitted during an NFC share operation. */