Хотите добавить в своё Android-приложение функцию бесконтактной оплаты, но не знаете, как это сделать? Тогда эта статья для вас! Заодно обсудим особенности реализации. В конце будет ссылка на репозиторий с примером.
Бесконтактная оплата
Как работает бесконтактная оплата и из каких этапов состоит её реализация, мы рассмотрели в предыдущей статье. Вкратце напомним, что для этого необходимо:
SDK, который обеспечивает защищённость и криптографические операции с данными.
Токенизация банковской карты с помощью SDK.
Сервис оплаты, который зарегистрирован определённым образом (ниже рассмотрим подробнее) для обмена APDU‑командами (Application Protocol Data Units) между терминалом и SDK.
При токенизации в Android-приложении применяется обычное клиент-серверное взаимодействие, поэтому здесь мы подробнее рассмотрим только сам сервис оплаты.
Сервис оплаты
При проведении транзакции через мобильное устройство приложение общается с POS-терминалом через сервис оплаты и передаёт ID токена вместо реальных данных карты, с одним платёжным ключом на терминал. Весь процесс оплаты выглядит так:
Чтобы Android-приложение могло общаться с терминалом, нужно указать разрешение в Manifest [1]: <uses-permission android:name="android.permission.NFC" />
, а затем реализовать сервис, как показано ниже:
class MyHostApduService : HostApduService() {
override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
...
}
override fun onDeactivated(reason: Int) {
...
}
}
Для начала оплаты мы ждём от терминала команду APDU. После её получения вызываем метод processCommandApdu
и передаём полученные данные на обработку в SDK. Все APDU определены в спецификации ISO/IEC 7816-4, это пакеты уровня приложения, которыми обмениваются считыватель NFC и сервис HostApduService
. Такой протокол является полудуплексным, то есть считыватель NFC отправляет вам команду APDU и ждёт APDU-ответ.
Когда удалённое устройство NFC хочет связаться с вашим сервисом, оно отправляет APDU SELECT AID
, как определено в спецификации ISO/IEC 7816-4. AID — это идентификатор приложения, процедура его регистрации определена в спецификации ISO/IEC 7816-5. Но если вы не хотите регистрировать свой AID, то можете использовать AID в собственном диапазоне, например, 0xF00102030405
.
В некоторых случаях сервису может потребоваться зарегистрировать несколько AID в одном приложении. Тогда сервис должен быть обработчиком по умолчанию для всех этих идентификаторов.
Бывают ситуации, когда другому сервису передаётся группа идентификаторов — список AID, которые Android рассматривает как связанные друг с другом: все вызовы идентификаторов из этой группы обязательно перенаправляются на один сервис.
Каждую группу AID можно связать с категорией. Это позволяет Android классифицировать сервисы, а пользователю — устанавливать значения по умолчанию на уровне категории, а не на уровне AID. Рассмотрим пример регистрации сервиса:
<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>
В строке meta-data
есть ссылка на файл, который нужен для связи каждой группы AID с категорией. Пример файла apduservice:
<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>
После этих шагов приложение уже будет реагировать на терминал.
Процесс обмена командами APDU между терминалом и сервисом должен быть максимально быстрым. Обсудим два варианта приложений: маленькое (из одного или нескольких модулей) и большое (многомодульное).
В случае одномодульного приложения обмен APDU будет быстрым, и нет большой необходимости переносить сервис в другой процесс.
А в большом приложении, скорее всего, понадобится переносить сервис оплаты в другой процесс, чтобы не замедлять запуск.
Правда, во втором случае может возникнуть новая проблема: если нужно синхронизировать данные, которые используются в разных процессах, то придётся синхронизировать и взаимодействие. Рассмотрим несколько способов решения.
Синхронизация данных
Допустим, вам нужно сохранить в файловом кеше какой-то примитив и позже считывать его в другом месте. Если приложение работает с одним процессом, то обычно применяют SharedPreferences. Но для работы с разными процессами этот механизм нам не подойдёт. В документации говорится:
Note: This class does not support use across multiple processes.
Первая мысль — использовать Content Provider, который идёт как обёртка вокруг 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
}
А сам класс SharedPreferences
может выглядеть так:
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()
}
}
Исходные код этого решения вы можете посмотреть здесь. Его преимущество в простоте: используются средства, которые предоставляет Android SDK. А главным недостатком является использование Content Provider-а, который работает в основном процессе приложения. Иными словами, каждый раз, когда мы общается к SharedPreferences
из другого процесса, будет подниматься основной процесс приложения, что влияет на производительность (потребление памяти, CPU и т. д).
Теперь давайте рассмотрим решение на основе библиотеки Harmony. Она реализует интерфейс SharedPreferences
, что позволяет легко использовать API в коде. Кроме того, для работы Harmony не требуется запуск каких-либо других процессов (Content Provider, Bound Service, AIDL и т. д.). Библиотека использует слушатель изменения файла и гарантирует, что данные map в памяти синхронизируются при каждом изменении. При этом все применяемые изменения упорядочены, так что можно одновременно вызывать изменения в нескольких процессах. Вот сравнение быстродействия нескольких библиотек:
Библиотека | Чтение | Запись | IPC |
SharedPreferences | 0,0006 мс | 0,066 мс | N/A |
Harmony | 0,0008 мс | 0,024 мс | 102,018 мс |
MMKV | 0,009 мс | 0,051 мс | 93,628 мс |
Tray | 2,895 мс | 8,225 мс | 1,928 сек. |
Библиотека MMKV использует нативный код, в Tray — решение на основе Content Provider-а. Как видите Harmony впереди по быстродействию, поэтому целесообразно использовать эту библиотеки.
Резюме
Мы обсудили:
Что необходимо для поддержки бесконтактной оплаты: SDK и сервис оплаты, который необходимо зарегистрировать в манифесте с указанием AID.
Особенности реализации сервиса оплаты в большом приложении. Для сохранения высокой скорости обмена командами APDU между терминалом и сервисом необходимо выносить сервис оплаты в другой процесс, чтобы уменьшить влияние на инициализацию приложения.
Проблему синхронизации данных между несколькими процессами в приложении и способы её решения. Проще всего использовать Content Provider как обёртку вокруг файловых данных. Но тогда нужно инициализировать второй процесс приложения, что приводит к низкой производительности. Самое оптимальное решение — библиотека Harmony, в основе которой лежит слушатель изменения в файле и не требуется Content Provider. Этот вариант показывает высокую скорость операций считывания и записи.
Посмотреть пример реализации бесконтактной оплаты можно по этой ссылке.