Skip to content

Communication via HCE (NFC) on Android

This guide explains how to enable communication with the Tapkey Mobile SDK using Android’s Host-based Card Emulation (HCE). Before proceeding, make sure to review the HCE Overview.

Familiarize Yourself with Android HCE APIs

This guide outlines how to integrate the Tapkey Mobile SDK with Android’s native Host-based Card Emulation (HCE) APIs.

Understanding the underlying Android HCE architecture is essential prior to implementation. Refer to the official Android documentation for detailed guidance on platform behavior, service declarations, and lifecycle expectations.

Info

The steps below — particularly those related to Android-specific configuration — are provided on a best-effort basis. Adaptations may be required depending on the application's structure, target Android version, or other platform constraints. Consulting the official Android documentation is strongly recommended.

Prerequisites

Ensure the following requirements are met:

  • AID (Application Identifier): A custom AID must be provisioned and maintained by the integrator.
  • Locks: Compatible locks must be available and pre-configured to accept the specified AID.

Project Configuration

1. Define a HostApduService

Communication via Host Card Emulation (HCE) is handled by an Android HostApduService.
This service is activated automatically when an NFC reader is detected and manages data exchange.

Create a class that extends HostApduService:

public class HceServiceImpl extends HostApduService {
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
        return new byte[0];
    }

    @Override
    public void onDeactivated(int reason) {
    }
}
class HceServiceImpl : HostApduService() {
    override fun processCommandApdu(
        commandApdu: ByteArray?,
        extras: Bundle?
    ): ByteArray? {
        return null
    }

    override fun onDeactivated(reason: Int) {
    }
}

2. Create apduservice.xml

Let the Android OS know your app supports HCE by defining supported AIDs and categories.

<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/app_name"
    android:requireDeviceUnlock="true">

    <aid-group
        android:category="other"
        android:description="@string/hce_aiddescription">
        <aid-filter android:name="YOUR_AID"/>
    </aid-group>

</host-apdu-service>

3. Register the Service in AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    ...

    <application>

        ...

        <service
            android:name="com.example.app.service.HceServiceImpl"
            android:exported="true"
            android:permission="android.permission.BIND_NFC_SERVICE">

            <intent-filter>
                <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
            </intent-filter>

            <meta-data
                android:name="android.nfc.cardemulation.host_apdu_service"
                android:resource="@xml/apduservice" />
        </service>

    </application>

</manifest>

4. Configure the AID in Tapkey Mobile SDK

Set the Application Identifier (AID) during SDK initialization to enable HCE-based communication:

TapkeyServiceFactory tapkeyServiceFactory = new TapkeyServiceFactoryBuilder(this)
    ...
    .setHceAid(YOUR_AID)
    .build();
val tapkeyServiceFactory = TapkeyServiceFactoryBuilder(this)
    ...
    .setHceAid(YOUR_AID)
    .build()

5. Implementing HCE Communication with the Tapkey Mobile SDK

To enable communication via Host Card Emulation (HCE) and trigger lock commands, continue by implementing the previously created HostApduService.

In the onCreate() method of the service, create a TapkeyHceConnectionHandler instance using
the HceConnectionHandlerFactory. This handler manages the NFC session lifecycle and
coordinates communication with the Tapkey SDK whenever a connection to an NFC reader is established.

Provide a callback function when creating the TapkeyHceConnectionHandler. This callback is invoked for every new connection to a Tapkey-enabled lock. Define the unlocking logic within this callback—for example, trigger a lock command, display status updates, or handle errors. This design allows full control over the behavior during NFC interactions.

In the processCommandApdu() method, forward all received data to the TapkeyHceConnectionHandler,
and pass the resulting response to sendResponseApdu(). This enables proper processing of the TLCP
protocol and ensures correct response handling.

When the system calls onDeactivated() (e.g., when the reader is removed or the session ends), invoke the onDeactivated() method on the TapkeyHceConnectionHandler to ensure proper session termination and cleanup.

public class HceServiceImpl extends HostApduService {
    private static final String TAG = HceServiceImpl.class.getSimpleName();

    private TapkeyHceConnectionHandler hceConnectionHandler;

    @Override
    public void onCreate() {
        super.onCreate();

        TapkeyAppContext appContext = (TapkeyAppContext) getApplication();
        TapkeyServiceFactory sf = appContext.getTapkeyServiceFactory();
        HceConnectionHandlerFactory hceConnectionHandlerFactory = sf.getHceConnectionHandlerFactory();
        CommandExecutionFacade commandExecutionFacade = sf.getCommandExecutionFacade();

        // This method is called when the service is first created.
        // The service may persist across multiple NFC reader sessions.

        // Create a new TapkeyHceConnectionHandler using the HceConnectionHandlerFactory.
        // This handler bridges the OS-level HCE APIs with the Tapkey Mobile SDK.
        // The provided function will be invoked each time a connection to an NFC reader is established.
        hceConnectionHandler = hceConnectionHandlerFactory.create(tlcpConnection -> {

            // A new connection to an NFC reader has been established.
            // Use the CommandExecutionFacade to send a command to the lock.

            return commandExecutionFacade.triggerLockAsync(tlcpConnection, CancellationTokens.None)
                .continueOnUi(commandResult -> {

                    switch (commandResult.getCommandResultCode()) {
                        case Ok:
                            // Command executed successfully
                            return null;

                        // Handle additional result cases as needed
                        ...
                    }

                    // Notify the user or handle unexpected results
                    ...
                    return null;
                })
                .catchOnUi(e -> {
                    Log.e(TAG, "Could not execute trigger lock command.", e);
                    return null;
                })
                .asVoid();
        });
    }

    @Override
    public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) {
        if (this.hceConnectionHandler == null)
            return ISO7816Constants.SW_FUNC_NOT_SUPPORTED;

        // Called by the OS when a command APDU is received from the NFC reader.
        // Forward the APDU to the TapkeyHceConnectionHandler for processing.

        hceConnectionHandler.transceiveAsync(commandApdu)
            .continueOnUi(rApdu -> {

                // The handler processes the command and returns a response APDU,
                // which must be sent back to the NFC reader.

                sendResponseApdu(rApdu);
                return null;
            })
            .catchOnUi(e -> {
                Log.e(TAG, "Couldn't process command APDU.", e);
                return null;
            })
            .conclude();

        return null;
    }

    @Override
    public void onDeactivated(int reason) {
        if (this.hceConnectionHandler == null)
            return;

        // Called by the OS when the connection to the NFC reader is lost or deactivated.
        // Notify the TapkeyHceConnectionHandler to clean up resources or reset state.
        this.hceConnectionHandler.onDeactivated();
    }
}
class HceServiceImpl : HostApduService() {

    private var hceConnectionHandler: TapkeyHceConnectionHandler? = null

    override fun onCreate() {
        super.onCreate()

        val appContext = application as TapkeyAppContext
        val sf = appContext.getTapkeyServiceFactory()
        val hceConnectionHandlerFactory = sf.getHceConnectionHandlerFactory()
        val commandExecutionFacade = sf.getCommandExecutionFacade()

        // This method is called when the service is first created.
        // The service may persist across multiple NFC reader sessions.

        // Create a new TapkeyHceConnectionHandler using the HceConnectionHandlerFactory.
        // This handler bridges the OS-level HCE APIs with the Tapkey Mobile SDK.
        // The provided function will be invoked each time a connection to an NFC reader is established.
        hceConnectionHandler = hceConnectionHandlerFactory.create { lock ->

            val commandResult: CommandResult

            try {
                commandResult = commandExecutionFacade.triggerLock(lock)
            } catch (e: Exception) {
                Log.e(TAG, "Something failed")
                return@create
            }

            // Notify the user or handle unexpected results

            when(commandResult.commandResultCode) {
                CommandResult.CommandResultCode.Ok -> {
                    // Command executed successfully
                    return@create
                }
                // Handle additional result cases as needed
            }
        }
    }

    override fun processCommandApdu(commandApdu: ByteArray?, extras: Bundle?): ByteArray? {
        if (hceConnectionHandler == null)
            return ISO7816Constants.SW_FUNC_NOT_SUPPORTED

        if (commandApdu == null)
            return null

        // Called by the OS when a command APDU is received from the NFC reader.
        // Forward the APDU to the TapkeyHceConnectionHandler for processing.

        CoroutineScope(Dispatchers.Main).launch {
            try {
                val rApdu = hceConnectionHandler?.transceive(commandApdu)

                // The handler processes the command and returns a response APDU,
                // which must be sent back to the NFC reader.

                if (rApdu != null)
                    sendResponseApdu(rApdu)
            } catch (e: Exception) {
                Log.e(TAG, "Couldn't process command APDU.", e)
            }
        }

        return null
    }

    override fun onDeactivated(reason: Int) {
        this.hceConnectionHandler?.onDeactivated()
    }
}

Initiating a Lock Communication

On Android, all HCE-based communication is handled through the implementation of HostApduService. This service acts as the system entry point for NFC interactions, even when the application is not in the foreground. It ensures that communication with a Tapkey-enabled lock can begin seamlessly upon NFC reader detection.

If a communication session is to be explicitly initiated from the user interface (e.g., in response to a user action within an Activity), it must still be routed through the HostApduService.

Note:

Directly overriding HCE handling within an Activity is not supported by the Android platform. To bridge between the foreground UI and the HCE service, a communication mechanism (e.g., via bound service, shared ViewModel, or broadcast receivers) must be implemented to pass control or data between the Activity and HostApduService.