NFC-мітки
AndroidBetaЗчитуйте й записуйте NFC-мітки зі свого застосунку для Android за допомогою NFC телефона або зчитувача термінала Sunmi серії P.
Зчитуйте й записуйте NFC-мітки, як-от картки лояльності, перепустки, наліпки та смарт-картки, безпосередньо зі свого застосунку для Android. SDK працює із власним зчитувачем пристрою: з Android NFC на телефоні або з безконтактним зчитувачем термінала Sunmi серії P. Жодного плагіна між ними немає.
Сесії з мітками постачаються в необов'язкових артефактах: com.tapaya:accept-nfc і, для терміналів Sunmi,
com.tapaya:accept-nfc-sunmi. Додавайте їх, лише якщо вони вам потрібні; платежі карткою працюють однаково з ними і без них.
Підтримувані пристрої
| Пристрій | Зчитувач | Потрібні артефакти |
|---|---|---|
| Телефон або планшет на Android з NFC | Android NFC | accept-nfc |
| Термінал Sunmi серії P | Безконтактний зчитувач термінала через платіжний сервіс Sunmi | accept-nfc і accept-nfc-sunmi |
| Будь-який інший пристрій | Немає | Не підтримується |
Термінали Sunmi серії P не мають Android NFC; їхня антена розташована за платіжним модулем. accept-nfc-sunmi
звертається до неї через платіжний сервіс Sunmi і замінює Android NFC щоразу, коли цей сервіс установлено.
На пристрої без придатного зчитувача isSupported() повертає false, а openSession() завершується помилкою
NOT_SUPPORTED.
Телефони мають кілька додаткових обмежень
Телефони зчитують мітки лише для активності (activity) на передньому плані, тому сесія завершується, коли ця активність
залишає передній план. MIFARE Classic працює лише на телефонах з NFC-контролером NXP; на інших його операції
завершуються помилкою NOT_SUPPORTED. Для міток ISO-DEP atsHex містить лише історичні байти ATS, оскільки Android
не надає решти.
Підтримувані мітки
| Тип мітки | Приклади |
|---|---|
| NFC Type 2 | NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C |
| MIFARE Classic | MIFARE Classic Mini, 1K, 4K |
| Смарт-картки ISO-DEP | MIFARE DESFire |
Як це працює
- Ваш застосунок викликає
Accept.nfc.openSession(activity). - SDK вмикає зчитувач пристрою і чекає на прикладання.
- Клієнт прикладає мітку, і
openSession()повертаєTagSessionз UID і сімейством мітки. - Ваш застосунок зчитує й записує дані через сесію, по одній команді за раз.
- Ваш застосунок викликає
close(), що звільняє зчитувач для платежів та інших сесій з мітками.
Платежі завжди мають пріоритет
Пристрій має один безконтактний зчитувач, який сесія з міткою та платіж карткою не можуть використовувати спільно.
Початок платежу через Accept.payments.pay() завершує вашу сесію, а openSession() завершується помилкою
READER_BUSY, доки платіж не закінчиться. Будьте готові до того, що сесія може завершитися будь-якої миті; див. Завершення сесії.
Передумови
- Підтримуваний пристрій.
- Accept SDK установлено й ініціалізовано. Виклик
isSupported()абоopenSession()доAccept.initializeвикидаєNotInitialized.
Сесії з мітками не потребують мерчанта, який увійшов у застосунок, плагіна Tapaya Terminal чи дозволу на доступ до геолокації. Артефакти самі додають до вашого маніфесту все потрібне:
accept-nfcдодає дозвілNFCі позначає функціюandroid.hardware.nfcяк необов'язкову, тож Play залишає ваш застосунок доступним на терміналах Sunmi, які не мають Android NFC.accept-nfc-sunmiдодає дозвілcom.sunmi.perm.CONTACTLESS_CARDі запис<queries>для платіжного сервісу Sunmi. Він містить вбудовану бібліотеку Sunmi PayLib.
Інтеграція
Установлення
dependencies {
implementation("com.tapaya:accept-nfc:1.20.0")
// Only if you run on Sunmi P-series terminals
implementation("com.tapaya:accept-nfc-sunmi:1.20.0")
}Якщо ви також оголошуєте com.tapaya:accept, тримайте всі артефакти на одній версії.
Accept.nfc є властивістю-розширенням (extension property); імпортуйте її перед використанням:
import com.tapaya.accept.nfc.nfcПеревірка підтримки
Викличте isSupported(), щоб вирішити, чи взагалі показувати функцію роботи з мітками:
if (Accept.nfc.isSupported()) {
showScanTagButton()
}isSupported() не потребує прикладання й не використовує зчитувач, тому її безпечно викликати будь-коли. Вона показує,
чи має пристрій зчитувач, який може використовувати SDK. На телефоні з вимкненим NFC вона повертає true; тоді
openSession() завершується помилкою NFC_DISABLED. Якщо Accept.initialize ще не виконано, вона викидає NotInitialized.
Відкриття сесії
Передайте активність, яка перебуває на передньому плані та в стані resumed:
val session = try {
Accept.nfc.openSession(activity)
} catch (e: TagSessionException) {
when (e.reason) {
TagErrorReason.TIMEOUT -> showMessage("No tag detected. Try again.")
TagErrorReason.READER_BUSY -> showMessage("The reader is busy. Try again soon.")
TagErrorReason.NFC_DISABLED -> promptToEnableNfc()
else -> showMessage("Can't read tags on this device.")
}
return
}
try {
showTag(session.info.uidHex, session.info.family)
// read and write...
} finally {
session.close()
}openSession() призупиняється, доки не буде прикладено мітку або не мине tapWait (за замовчуванням 30 секунд). Поки триває очікування,
показуйте підказку «Прикладіть картку». Цей метод викидає IllegalStateException, якщо activity не перебуває в стані resumed.
На телефоні активність має значення: Android зчитує мітки лише для неї, а вихід з переднього плану зупиняє очікування або
завершує сесію з причиною ACTIVITY_PAUSED. Термінали Sunmi зчитують мітки незалежно від того, що перебуває на передньому
плані, але активність однаково потрібно передати.
Щоб припинити очікування, наприклад коли клієнт натискає Cancel, скасуйте корутину, яка викликала openSession().
Зчитувач припиняє очікування, і ви можете одразу відкрити нову сесію.
Отримання інформації про сесію
session.info описує прикладену мітку:
| Поле | Опис |
|---|---|
uidHex | UID мітки як шістнадцятковий рядок у верхньому регістрі, наприклад 04A1B2C3D4E5F6. |
family | Значення TagFamily, яке показує, які операції застосовні. |
sak, atqaHex | Низькорівнева ідентифікація, що повертається під час виявлення мітки. |
atsHex | ATS для міток ISO_DEP; інакше null. |
memoryBytes | Розмір пам'яті користувача, якщо зчитувач може його визначити; інакше null. |
versionHex | Відповідь на GET_VERSION для міток, які її підтримують; інакше null. |
Зчитування й запис мітки
Розгалужте логіку за session.info.family, а потім використовуйте операції для цього типу мітки:
when (session.info.family) {
TagFamily.NTAG, TagFamily.ULTRALIGHT, TagFamily.ULTRALIGHT_EV1,
TagFamily.ULTRALIGHT_C, TagFamily.TYPE2_OTHER -> readType2Tag(session)
TagFamily.MIFARE_CLASSIC_MINI, TagFamily.MIFARE_CLASSIC_1K,
TagFamily.MIFARE_CLASSIC_4K -> readClassicCard(session)
TagFamily.ISO_DEP -> readSmartCard(session)
else -> showMessage("This tag isn't supported.")
}Команда, яка не відповідає прикладеній мітці, завершується помилкою NOT_SUPPORTED, а сесія залишається відкритою.
NFC Type 2
NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C та інші мітки Type 2 (TYPE2_OTHER).
| Операція | Що робить |
|---|---|
readPages(startPage) | Повертає 16 байтів: чотири 4-байтові сторінки, починаючи зі сторінки startPage. |
writePage(page, data) | Записує одну 4-байтову сторінку. |
passwordAuth(password) | Лише NTAG і Ultralight EV1. Розблоковує захищені сторінки 4-байтовим паролем і повертає 2-байтовий PACK, за допомогою якого можна перевірити, що мітка справжня. |
ultralightCAuth(key) | Лише Ultralight C. Виконує автентифікацію 16-байтовим ключем 3DES. |
transceiveType2(frame) | Надсилає необроблений кадр Type 2 для команд, що не мають окремого методу, і повертає відповідь мітки. |
// Protected NTAG and Ultralight EV1 tags only
session.passwordAuth(password = byteArrayOf(0x12, 0x34, 0x56, 0x78))
val pages = session.readPages(startPage = 4) // pages 4 to 7
session.writePage(page = 4, data = byteArrayOf(0x01, 0x02, 0x03, 0x04))
// Raw GET_VERSION command
val version = session.transceiveType2(byteArrayOf(0x60))MIFARE Classic
MIFARE Classic Mini, 1K і 4K. Перш ніж зчитувати або записувати блоки сектора, пройдіть автентифікацію для цього сектора.
| Операція | Що робить |
|---|---|
authenticateClassic(block, keyType, key) | Автентифікує 6-байтовим ключем сектор, що містить block. |
readBlock(block) | Повертає 16 байтів блоку. |
writeBlock(block, data) | Записує 16 байтів у блок. |
increment, decrement, restore | Поміщають результат операції з блоком значення в буфер передавання мітки. |
transfer(block) | Записує буфер передавання в блок. |
session.authenticateClassic(block = 4, keyType = MifareKeyType.A, key = sectorKey)
val block = session.readBlock(4)
session.writeBlock(4, newContent)
// Value blocks: change the value, then write it back
session.decrement(block = 5, value = 1)
session.transfer(block = 5)Автентифікація неправильним ключем завершується помилкою AUTH_FAILED, і SDK повторно вибирає мітку (re-select) за вас, тож ви можете одразу спробувати наступний
ключ:
val key = candidateKeys.firstOrNull { key ->
try {
session.authenticateClassic(block = 4, keyType = MifareKeyType.A, key = key)
true
} catch (e: TagSessionException) {
if (e.reason != TagErrorReason.AUTH_FAILED) throw e
false
}
} ?: error("no key for sector 1")Смарт-картки ISO-DEP
MIFARE DESFire та інші картки ISO 14443-4.
| Операція | Що робить |
|---|---|
transmitApdu(apdu) | Надсилає одну командну APDU і повертає відповідь: дані, за якими йдуть SW1 SW2. |
val select = byteArrayOf(0x00, 0xA4.toByte(), 0x04, 0x00, 0x07,
0xA0.toByte(), 0x00, 0x00, 0x00, 0x03, 0x10, 0x10, 0x00)
val response = session.transmitApdu(select)
val sw = response.takeLast(2) // [0x90, 0x00] on successЗавершення сесії
Викличте close(), щойно закінчите, бажано з блоку finally. Доки ви цього не зробите, жоден платіж не зможе використати
зчитувач. close() безпечно викликати кілька разів, і для сесії, яка вже завершилася, він ніколи не викидає винятків.
Сесія також може завершитися сама. session.ended є StateFlow, який залишається null, поки сесія активна,
а потім надсилає причину її завершення:
session.ended.filterNotNull().first().let { reason ->
when (reason) {
TagSessionEndReason.CARD_REMOVED -> showMessage("Card removed")
TagSessionEndReason.PAYMENT -> showMessage("A payment took over the reader")
TagSessionEndReason.ACTIVITY_PAUSED -> showMessage("Keep the app in the foreground")
else -> Unit
}
}TagSessionEndReason | Коли виникає |
|---|---|
CLOSED | Ваш застосунок викликав close(). |
CARD_REMOVED | Мітка вийшла з поля зчитувача й не повернулася протягом тайм-ауту reselect. |
IDLE_TIMEOUT | Протягом тайм-ауту idle (за замовчуванням 10 секунд) не надійшло жодної команди. |
PAYMENT | Почався платіж через Accept.payments.pay(). Платежі завжди мають пріоритет. |
ACTIVITY_PAUSED | Лише телефони. Активність, з якою ви відкрили сесію, залишила передній план, і Android припинив зчитувати мітки для неї. |
READER_LOST | Лише термінали Sunmi. З'єднання з платіжним сервісом Sunmi обірвалося. |
Після завершення сесії кожна наступна команда завершується помилкою SESSION_ENDED. Для нового прикладання знову викличте
openSession().
Обробка помилок
Збої під час роботи з міткою викидають TagSessionException, чий reason підказує, що робити далі. Виклик API до
налаштування SDK викидає NotInitialized. TagSessionException не є AcceptException, тому перехоплюйте ці
два винятки окремо:
try {
val session = Accept.nfc.openSession(activity)
// ...
} catch (e: TagSessionException) {
// reason-specific handling, see the tables below
} catch (e: NotInitialized) {
// Accept.initialize hasn't been called
}Повторіть автентифікацію, якщо встановлено authLost
За будь-якої причини може бути встановлено прапорець винятку authLost. Якщо його встановлено, пройдіть автентифікацію повторно, перш ніж надсилати
наступну команду до захищеної пам'яті; на DESFire також повторно виберіть застосунок.
Аргумент ключа, блоку чи сторінки неправильної довжини або знака, а також порожній кадр чи APDU, спричиняють
IllegalArgumentException ще до того, як будь-що дійде до мітки.
Відкриття сесії
reason | Коли виникає | Що робити |
|---|---|---|
TIMEOUT | Протягом tapWait не було прикладання. | Знову попросіть прикласти мітку або припиніть спробу. |
READER_BUSY | Зчитувачем користується платіж або інша сесія. | Повторіть спробу пізніше; черги немає. |
SESSION_ENDED | Під час очікування прикладання почався платіж або активність залишила передній план. | Відкрийте нову сесію, коли платіж завершиться або застосунок повернеться на передній план. |
NFC_DISABLED | Лише телефони. NFC вимкнено в системних налаштуваннях. | Попросіть користувача ввімкнути NFC. |
VENDOR_ERROR | Термінали Sunmi: платіжний сервіс недоступний. vendorCode містить його код, якщо сервіс його надав. | Повторіть спробу. Якщо помилка не зникає, повідомте про неї підтримці для розробників, указавши vendorCode. |
NOT_SUPPORTED | Пристрій не має зчитувача, який може використовувати SDK. | Приховайте функцію, коли isSupported() повертає false. |
Надсилання команди
reason | Коли виникає | Що робити |
|---|---|---|
TIMEOUT | Мітка не відповіла вчасно; її повторно вибрано. | Повторіть спробу. Якщо встановлено authLost, спершу пройдіть автентифікацію. |
NO_RESPONSE | Мітка не відповіла. | Повторіть спробу, якщо мітка досі в полі зчитувача. |
AUTH_FAILED | Неправильний ключ або пароль. | Спробуйте наступний ключ; мітку вже повторно вибрано. |
NAK | Мітка відхилила команду: сторінка поза діапазоном, захищена пам'ять або непідтримувана команда. | Виправте команду. Сесія триває. |
NOT_SUPPORTED | Команда не відповідає цьому сімейству міток або NFC-контролеру цього телефона (MIFARE Classic потребує NXP). | Перевірте session.info.family. Сесія триває. |
VENDOR_ERROR | Власний SDK зчитувача повідомив про несправність; vendorCode містить її код. | Повторіть спробу, а потім повідомте про це підтримці для розробників, указавши vendorCode. |
Сесію завершено
reason | Коли виникає | Що робити |
|---|---|---|
CARD_REMOVED | Мітка вийшла з поля зчитувача під час виконання команди. | Попросіть прикласти мітку знову й відкрийте нову сесію. |
SESSION_ENDED | Команду надіслано після завершення сесії. | Відкрийте нову сесію. |
Тайм-аути
Щоб змінити значення за замовчуванням, передайте TagTimeouts в openSession():
Accept.nfc.openSession(activity, TagTimeouts(tapWait = 15.seconds, idle = 30.seconds))| Тайм-аут | За замовчуванням | Максимум | Стосується |
|---|---|---|---|
tapWait | 30 с | 60 с | Очікування прикладання в openSession() |
operation | 1 с | 5 с | Кожної команди на мітці |
reselect | 1 с | 5 с | Повторного вибору мітки після невдалої команди |
idle | 10 с | 60 с | Часу між командами, після якого сесія завершується |
Кожне значення має бути більшим за нуль. Створення TagTimeouts зі значенням поза цими діапазонами викидає
IllegalArgumentException.
Корисно знати
- Один зчитувач. Пристрій має один безконтактний зчитувач, тому одночасно можлива лише одна сесія з міткою або одне
очікування прикладання. Другий виклик
openSession()завершується помилкоюREADER_BUSY. Платежі, розпочаті черезAccept.payments.pay(), спершу завершують сесію; платежі, розпочаті поза SDK, узгодити неможливо, тому робіть сесії короткими. - Скасування команди не відміняє її. На момент, коли ви скасовуєте корутину, команда вже дійшла до мітки
й усе одно виконується; відкидається лише її результат. Скасований
writeBlockусе ще може змінити мітку. - Команди виконуються по черзі. Методи сесії можна викликати з будь-якої корутини; вони доходять до мітки по одній, у тому порядку, у якому ви їх викликаєте.
- Дані міток не потрапляють у логи. SDK ніколи не записує в логи ключі, паролі чи дані міток і маскує UID (наприклад,
04A1…E5F6). Як ваш застосунок зберігає й показує дані міток, вирішуєте ви.
Тестування інтеграції
На телефоні та, якщо ви їх підтримуєте, на терміналі Sunmi серії P:
- Перевірте, що
isSupported()повертаєtrueна обох пристроях іfalseна пристрої без NFC. - Прикладіть мітку NTAG або Ultralight і перевірте, що сесія повідомляє правильний UID, а
readPages(0)повертає 16 байтів. Потім виконайтеwritePageі зчитайте сторінку знову. - На MIFARE Classic пройдіть автентифікацію неправильним ключем і переконайтеся, що отримано
AUTH_FAILED, а потім правильним. - Скасуйте
openSession()під час очікування прикладання й одразу відкрийте нову сесію. - Почніть платіж під час сесії: сесія має завершитися як
PAYMENT, а платіж має пройти. Заберіть мітку під час іншої сесії: сесія має завершитися якCARD_REMOVED. - На телефоні вимкніть NFC і переконайтеся, що отримано
NFC_DISABLED. Під час сесії переведіть застосунок у фоновий режим і переконайтеся, що сесія завершується якACTIVITY_PAUSED.