Android HCE: setPreferredService() silently ignored despite foreground call, HostApduService never invoked


Background

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.

Problem

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.

Setup

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>

HostApduService

class 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.

What I've verified

  • 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

What I've tried

  1. Setting our app as the default NFC payment app via system settings → no change

  2. Re-calling setPreferredService every 10 seconds → no change

  3. Re-calling on AppState 'active' (foreground returns) → no change

  4. Verifying isDefaultServiceForAid() after setPreferredService → returns false (but this might be expected since isDefaultServiceForAid is about default app, not foreground preference)

  5. Force-stopping Samsung Wallet before testing → no change on some devices

Affected 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

Environment

  • 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

Question

Per the official documentation, setPreferredService() in the foreground should override AID conflict resolution. Yet our HCE service is never invoked on many Samsung devices.

  1. Are there any undocumented OEM-specific requirements (signing, manifest declarations, permissions) that Samsung's One UI checks before honoring setPreferredService?

  2. 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.

  3. Does Android 15's "Wallet role holder" concept silently override setPreferredService even on Android 14 devices via system updates?

  4. 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.

1
May 28 at 6:27 AM
User Avataruser26755203
#android#react-native#nfc

No answer found for this question yet.