Android HCE: setPreferredService() silently ignored despite foreground call, HostApduService never invoked
I have a transit card HCE app that emulates Korean KS X 6924 transit cards (T-money/Cashbee category AIDs). The app's HostApduService registers 6 transit AIDs under category="payment".
When the user opens the payment screen, we call setPreferredService() so our service takes foreground priority over Samsung Wallet / Google Pay.
According to the official Android documentation:
> "Apps in the foreground can invoke setPreferredService to specify which card emulation service should be preferred while a specific activity is in the foreground. This foreground app preference overrides the AID conflict resolution."
>
> "Apps registering AIDs under CATEGORY_PAYMENT can only process a transaction if [...] The app is in the foreground and invokes setPreferredService."
So in theory, our service should be invoked when the user taps a charger while our payment screen is in the foreground.
On approximately 50% of Samsung Galaxy devices, our HostApduService.processCommandApdu() is never called, even when:
Our payment screen is actively in the foreground
setPreferredService() was successfully called (no exception thrown)
NFC is enabled
User has even manually set our app as the default NFC payment app
The user taps the charger, sees "Authentication Failed", and our internal APDU log (SharedPreferences-based, written in processCommandApdu) is completely empty — indicating the OS never routed the SELECT APDU to our service.
On the other 50% of devices, everything works correctly.
apdu_service.xml<host-apdu-service xmlns:android="<http://schemas.android.com/apk/res/android>"
android:description="@string/app_name"
android:requireDeviceUnlock="false">
<aid-group android:description="@string/app_name" android:category="payment">
<aid-filter android:name="D4100000030001"/> <!-- KS X 6924-1 -->
<aid-filter android:name="D4100000140001"/> <!-- T-money 부가 -->
<aid-filter android:name="D4100000300001"/> <!-- Cashbee -->
<aid-filter android:name="A000000003969807"/> <!-- KS X 6924-2 -->
<aid-filter android:name="D4100000020001"/> <!-- Transit standard -->
<aid-filter android:name="D410000003000101"/> <!-- T-money B -->
</aid-group>
</host-apdu-service>
HostApduServiceclass PitinHceService : HostApduService() {
override fun processCommandApdu(commandApdu: ByteArray?, extras: Bundle?): ByteArray {
// Log every invocation to SharedPreferences for diagnostics
appendLog(this, "RX " + bytesToHex(commandApdu ?: byteArrayOf()))
// ... build KS X 6924 frame response
}
}
setPreferredService call@ReactMethod
fun setPreferredService(promise: Promise) {
try {
val activity = reactApplicationContext.currentActivity
?: return promise.reject("NO_ACTIVITY", "No current activity")
val adapter = NfcAdapter.getDefaultAdapter(reactApplicationContext)
?: return promise.reject("NO_NFC", "NFC not available")
val emulation = CardEmulation.getInstance(adapter)
val component = ComponentName(reactApplicationContext, PitinHceService::class.java)
emulation.setPreferredService(activity, component)
promise.resolve(true)
} catch (e: Exception) {
promise.reject("PREFERRED_ERROR", e)
}
}
Called from JS layer when payment screen mounts, and re-called every 10 seconds while the screen is active.
setPreferredService() returns successfully (no exception) on failing devices
NFC is enabled (NfcAdapter.isEnabled() returns true)
The payment screen's hosting activity (MainActivity in React Native) is the resumed activity at the time of the call
processCommandApdu is never called — confirmed by empty APDU log in SharedPreferences
The same charger and same user account work fine on different devices
Setting our app as the default NFC payment app via system settings → no change
Re-calling setPreferredService every 10 seconds → no change
Re-calling on AppState 'active' (foreground returns) → no change
Verifying isDefaultServiceForAid() after setPreferredService → returns false (but this might be expected since isDefaultServiceForAid is about default app, not foreground preference)
Force-stopping Samsung Wallet before testing → no change on some devices
Samsung Galaxy S22, S23 (One UI 6.x) — frequently fails
Samsung Galaxy A series (various) — frequently fails
Samsung Galaxy Z Flip 5 — sometimes works, sometimes fails
Google Pixel — usually works
LG / Xiaomi — works
React Native 0.81.5 (Bare Workflow)
Expo SDK 54
Target SDK 35, Min SDK 24
React Native New Architecture enabled (newArchEnabled: true)
NFC permission and feature declared in manifest
Per the official documentation, setPreferredService() in the foreground should override AID conflict resolution. Yet our HCE service is never invoked on many Samsung devices.
Are there any undocumented OEM-specific requirements (signing, manifest declarations, permissions) that Samsung's One UI checks before honoring setPreferredService?
Is there a way to verify at runtime whether setPreferredService is actually being honored by the OS (other than waiting for a real charger interaction)? The public CardEmulation API doesn't seem to expose this.
Does Android 15's "Wallet role holder" concept silently override setPreferredService even on Android 14 devices via system updates?
How do production HCE apps in Korea (KB Pay, Shinhan Sol Pay, etc.) successfully bypass Samsung Wallet interception? Is it via Samsung KNOX partnership or specific signing certificates?
Any insight into why setPreferredService is being silently ignored — or a way to debug this — would be greatly appreciated.