how to add contactless payment support to an Android application

Do you want to add contactless payment functionality to your Android app, but don’t know how? Then this article is for you! At the same time, we will discuss the implementation features. At the end there will be a link to the repository with an example.

Contactless payment

How contactless payment works and what stages its implementation consists of, we examined in previous article. Let us briefly recall what is needed for this:

  • SDK, which provides security and cryptographic operations with data.

  • Bank card tokenization using SDK.

  • A payment service that is registered in a certain way (we’ll look at it in more detail below) for the exchange of APDU commands (Application Protocol Data Units) between the terminal and the SDK.

Tokenization in an Android application uses the usual client-server interaction, so here we will take a closer look at only the payment service itself.

Payment service

When conducting a transaction through a mobile device, the application communicates with the POS terminal through the payment service and transmits the token ID instead of real card data, with one payment key to the terminal. The entire payment process looks like this:

In order for an Android application to communicate with the terminal, you need to specify the permission in the Manifest [1]: <uses-permission android:name="android.permission.NFC" />and then implement the service as below:

class MyHostApduService : HostApduService() {

    override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
       ...
    }

    override fun onDeactivated(reason: Int) {
       ...
    }
}

To start payment, we are waiting for the APDU command from the terminal. After receiving it, call the method processCommandApdu and transfer the received data for processing to the SDK. All APDUs are defined in the ISO/IEC 7816-4 specification, these are application layer packets exchanged between the NFC reader and the service HostApduService. This protocol is half-duplex, that is, the NFC reader sends you an APDU command and waits for an APDU response.

When a remote NFC device wants to contact your service, it sends an APDU SELECT AID, as defined in the ISO/IEC 7816-4 specification. AID is an application identifier and its registration procedure is defined in the ISO/IEC 7816-5 specification. But if you don’t want to register your AID, you can use an AID in your own range, e.g. 0xF00102030405.

In some cases, a service may need to register multiple AIDs in a single application. Then service must be the default handler for all these identifiers.

There are situations when a group of identifiers is passed to another service – a list of AIDs that Android considers as related to each other: all calls to identifiers from this group are necessarily redirected to one service.

Each AID group can be associated with a category. This allows Android to categorize services and allows the user to set defaults at the category level rather than at the AID level. Let’s consider service registration example:

<service android:name=".MyHostApduService" 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>

In line meta-data there is a link to the file that is needed to associate each AID group with a category. Example apduservice file:

<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
           android:description="@string/servicedesc" android:requireDeviceUnlock="false">
       <aid-group android:description="@string/aiddescription" android:category="other">
           <aid-filter android:name="F0010203040506"/>
           <aid-filter android:name="F0394148148100"/>
       </aid-group>
 </host-apdu-service> 

After these steps, the application will already respond to the terminal.

The process of exchanging APDU commands between the terminal and the service should be as fast as possible. Let’s discuss two application options: small (one or several modules) and large (multi-module).

  • In the case of a single-module application, the exchange of APDUs will be fast, and there is little need to move the service to another process.

  • And in a large application, most likely, you will need to transfer the payment service to another process so as not to slow down the launch.

However, in the second case, a new problem may arise: if you need to synchronize data that is used in different processes, then you will have to synchronize the interaction. Let’s consider several solutions.

Data synchronization

Let’s say you need to store some primitive in the file cache and later read it in another location. If the application works with one process, then it is usually used SharedPreferences. But this mechanism will not suit us for working with different processes. The documentation says:

Note: This class does not support use across multiple processes.

The first thought is to use a Content Provider, which comes as a wrapper around SharedPreferences:

override fun insert(uri: Uri, values: ContentValues?): Uri? {
        when (matcher.match(uri)) {
            MATCH_DATA -> {
                val editor: SharedPreferences.Editor =
                    PreferenceManager.getDefaultSharedPreferences(context).edit()
                if (values?.valueSet() != null) {
                    for ((key, value) in values.valueSet()) {
                        editor.putString(key, value as? String?)
                    }
                    editor.apply()
                }
            }

            else -> throw IllegalArgumentException("Unsupported uri $uri")
        }
        return null
    }

And the class itself SharedPreferences might look like this:

class MultiprocessSharedPreferences private constructor(private val context: Context) {
        fun edit(): Editor {
            return Editor(context)
        }

        fun getString(key: String?, def: String?): String? {
            val cursor = context.contentResolver.query(getContentUri(context, key, STRING_TYPE), null, null, null, null)
            return getStringValue(cursor, def)
        }

        private fun getContentUri(context: Context, key: String, type: String): Uri {
            if (BASE_URI == null) {
                init(context)
            }
            return BASE_URI.buildUpon().appendPath(key).appendPath(type).build()
        }
    }

You can see the source code for this solution Here. Its advantage is simplicity: it uses the tools provided by the Android SDK. The main disadvantage is the use of a Content Provider, which runs in the main application process. In other words, every time we communicate with SharedPreferences from another process, the main application process will be elevated, which affects performance (memory consumption, CPU, etc.).

Now let’s look at a solution based on Harmony libraries. It implements the interface SharedPreferences, making it easy to use the API in code. In addition, Harmony does not require any other processes to run (Content Provider, Bound Service, AIDL, etc.). The library uses a file change listener and ensures that the map data in memory is synchronized with each change. This keeps all applied changes in order so that changes can be made to multiple processes at the same time. Here is a comparison of the performance of several libraries:

Library

Reading

Record

IPC

SharedPreferences

0.0006 ms

0.066 ms

N/A

Harmony

0.0008 ms

0.024 ms

102.018 ms

MMKV

0.009 ms

0.051 ms

93.628 ms

Tray

2.895 ms

8.225 ms

1.928 sec.

Library MMKV uses native code, in Tray — a solution based on Content Provider. As you can see, Harmony is ahead in terms of performance, so it is advisable to use this library.

Summary

We discussed:

  • What is needed to support contactless payment: SDK and payment service, which must be registered in the manifest indicating the AID.

  • Features of implementing a payment service in a large application. To maintain a high speed of exchange of APDU commands between the terminal and the service, it is necessary to move the payment service to another process in order to reduce the impact on application initialization.

  • The problem of data synchronization between several processes in an application and ways to solve it. The easiest way is to use the Content Provider as a wrapper around file data. But then a second application process needs to be initialized, which results in poor performance. The most optimal solution is the Harmony library, which is based on a file change listener and does not require a Content Provider. This option shows high speed of read and write operations.

You can see an example of implementing contactless payment at this link.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *