NFC tagy
AndroidBetaČtěte a zapisujte NFC tagy ze své aplikace pro Android pomocí NFC telefonu nebo čtečky terminálu Sunmi řady P.
Čtěte a zapisujte NFC tagy, například věrnostní karty, přístupové karty, nálepky a čipové karty, přímo ze své aplikace pro Android. SDK komunikuje přímo s čtečkou zařízení: na telefonu s Android NFC, na terminálu Sunmi řady P s jeho bezkontaktní čtečkou. Mezi SDK a čtečkou nestojí žádný plugin.
Relace s tagy jsou součástí volitelných artefaktů com.tapaya:accept-nfc a pro terminály Sunmi
com.tapaya:accept-nfc-sunmi. Přidejte je jen tehdy, když je potřebujete; platby kartou fungují s nimi i bez nich stejně.
Podporovaná zařízení
| Zařízení | Čtečka | Potřebné artefakty |
|---|---|---|
| Telefon nebo tablet s Androidem a NFC | Android NFC | accept-nfc |
| Terminál Sunmi řady P | Bezkontaktní čtečka terminálu, přes platební službu Sunmi | accept-nfc a accept-nfc-sunmi |
| Jakékoli jiné zařízení | Žádná | Nepodporováno |
Terminály Sunmi řady P nemají Android NFC; jejich anténa je umístěna za platebním modulem. accept-nfc-sunmi
k ní přistupuje přes platební službu Sunmi, a kdykoli je tato služba nainstalovaná, má přednost před Android NFC.
Na zařízení bez použitelné čtečky vrací isSupported() hodnotu false a openSession() selže s
NOT_SUPPORTED.
Telefony mají několik omezení navíc
Telefony čtou tagy jen pro aktivitu v popředí, takže relace skončí, jakmile tato aktivita popředí opustí.
MIFARE Classic funguje jen na telefonech s NFC řadičem NXP; jinde jeho operace selžou s
NOT_SUPPORTED. U tagů ISO-DEP obsahuje atsHex jen historické bajty ATS, protože zbytek Android
nezpřístupňuje.
Podporované tagy
| Typ tagu | Příklady |
|---|---|
| NFC Type 2 | NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C |
| MIFARE Classic | MIFARE Classic Mini, 1K, 4K |
| Čipové karty ISO-DEP | MIFARE DESFire |
Jak to funguje
- Vaše aplikace zavolá
Accept.nfc.openSession(activity). - SDK zapne čtečku zařízení a čeká na přiložení.
- Zákazník přiloží tag a
openSession()vrátíTagSessions UID a rodinou tagu. - Vaše aplikace čte a zapisuje prostřednictvím relace, vždy po jednom příkazu.
- Vaše aplikace zavolá
close(), čímž uvolní čtečku pro platby a další relace s tagy.
Platby mají vždy přednost
Zařízení má jednu bezkontaktní čtečku, kterou relace s tagem a platba kartou nemohou sdílet. Zahájení platby
pomocí Accept.payments.pay() vaši relaci ukončí a openSession() selhává s READER_BUSY, dokud platba
neskončí. Počítejte s tím, že relace může skončit kdykoli; viz Ukončení relace.
Předpoklady
- Podporované zařízení.
- Accept SDK nainstalované a inicializované. Volání
isSupported()neboopenSession()předAccept.initializevyhodíNotInitialized.
Relace s tagy nevyžadují přihlášeného obchodníka, plugin Tapaya Terminal ani oprávnění k poloze. Artefakty si do vašeho manifestu přidají, co potřebují:
accept-nfcpřidá oprávněníNFCa označíandroid.hardware.nfcjako nepovinné, takže Google Play ponechá vaši aplikaci dostupnou i na terminálech Sunmi, které Android NFC nemají.accept-nfc-sunmipřidá oprávněnícom.sunmi.perm.CONTACTLESS_CARDa záznam<queries>pro platební službu Sunmi. Obsahuje knihovnu Sunmi PayLib.
Integrace
Instalace
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")
}Pokud deklarujete i com.tapaya:accept, udržujte všechny artefakty ve stejné verzi.
Accept.nfc je extension property; před použitím ji naimportujte:
import com.tapaya.accept.nfc.nfcOvěření podpory
Zavolejte isSupported() a podle výsledku rozhodněte, zda funkci pro tagy vůbec zobrazit:
if (Accept.nfc.isSupported()) {
showScanTagButton()
}isSupported() nevyžaduje přiložení a čtečku nijak nevyužívá, takže ji můžete bezpečně volat kdykoli. Říká vám,
zda má zařízení čtečku, kterou SDK dokáže použít. Na telefonu s vypnutým NFC vrací true; openSession() pak
selže s NFC_DISABLED. Pokud Accept.initialize ještě neproběhlo, vyhodí metoda výjimku NotInitialized.
Otevření relace
Předejte aktivitu, která je v popředí a ve stavu 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() se pozastaví, dokud není přiložen tag nebo dokud nevyprší tapWait (ve výchozím nastavení 30 sekund). Během čekání
zobrazte výzvu „Přiložte kartu“. Pokud activity není ve stavu resumed, vyhodí metoda výjimku IllegalStateException.
Na telefonu na aktivitě záleží: Android čte tagy jen pro ni a jakmile opustí popředí, čekání se zastaví nebo relace
skončí s ACTIVITY_PAUSED. Terminály Sunmi čtou tagy bez ohledu na to, co je v popředí, aktivitu ale předáváte i tak.
Chcete-li čekání ukončit, například když zákazník klepne na Cancel, zrušte korutinu, která zavolala openSession().
Čtečka přestane čekat a můžete hned otevřít novou relaci.
Čtení informací o relaci
session.info popisuje přiložený tag:
| Pole | Popis |
|---|---|
uidHex | UID tagu jako hexadecimální řetězec velkými písmeny, např. 04A1B2C3D4E5F6. |
family | Hodnota TagFamily, ze které poznáte, jaké operace lze použít. |
sak, atqaHex | Nízkoúrovňová identifikace vrácená při detekci tagu. |
atsHex | ATS pro tagy ISO_DEP; jinak null. |
memoryBytes | Velikost uživatelské paměti, pokud ji čtečka dokáže zjistit; jinak null. |
versionHex | Odpověď na GET_VERSION u tagů, které ji podporují; jinak null. |
Čtení a zápis tagu
Větvěte podle session.info.family a poté použijte operace pro daný typ tagu:
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.")
}Příkaz, který neodpovídá přiloženému tagu, selže s NOT_SUPPORTED a relace zůstane otevřená.
NFC Type 2
NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C a další tagy Type 2 (TYPE2_OTHER).
| Operace | Co dělá |
|---|---|
readPages(startPage) | Vrátí 16 bajtů: čtyři 4bajtové stránky počínaje stránkou startPage. |
writePage(page, data) | Zapíše jednu 4bajtovou stránku. |
passwordAuth(password) | Jen NTAG a Ultralight EV1. Odemkne chráněné stránky 4bajtovým heslem a vrátí 2bajtový PACK, pomocí kterého můžete ověřit, že je tag pravý. |
ultralightCAuth(key) | Jen Ultralight C. Autentizuje se 16bajtovým klíčem 3DES. |
transceiveType2(frame) | Odešle surový rámec Type 2 pro příkazy bez vlastní metody a vrátí odpověď tagu. |
// 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 a 4K. Než budete číst nebo zapisovat bloky sektoru, sektor autentizujte.
| Operace | Co dělá |
|---|---|
authenticateClassic(block, keyType, key) | Autentizuje 6bajtovým klíčem sektor, který obsahuje block. |
readBlock(block) | Vrátí 16 bajtů bloku. |
writeBlock(block, data) | Zapíše do bloku 16 bajtů. |
increment, decrement, restore | Uloží výsledek operace s hodnotovým blokem do přenosového bufferu tagu. |
transfer(block) | Zapíše přenosový buffer do bloku. |
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)Autentizace nesprávným klíčem selže s AUTH_FAILED a SDK za vás tag znovu vybere (re-select), takže můžete ihned zkusit další
klíč:
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")Čipové karty ISO-DEP
MIFARE DESFire a další karty ISO 14443-4.
| Operace | Co dělá |
|---|---|
transmitApdu(apdu) | Odešle jedno příkazové APDU a vrátí odpověď: data následovaná 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 successUkončení relace
Jakmile skončíte, zavolejte close(), ideálně z bloku finally. Dokud to neuděláte, žádná platba nemůže čtečku
použít. close() je bezpečné volat opakovaně a u relace, která už skončila, nikdy nevyhazuje výjimku.
Relace může skončit i sama. session.ended je StateFlow, který zůstává null, dokud je relace aktivní,
a poté vyšle důvod jejího ukončení:
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 | Kdy nastává |
|---|---|
CLOSED | Vaše aplikace zavolala close(). |
CARD_REMOVED | Tag opustil pole čtečky a během časového limitu reselect se nevrátil. |
IDLE_TIMEOUT | Během časového limitu idle (ve výchozím nastavení 10 sekund) nepřišel žádný příkaz. |
PAYMENT | Byla zahájena platba přes Accept.payments.pay(). Platby mají vždy přednost. |
ACTIVITY_PAUSED | Jen telefony. Aktivita, se kterou jste relaci otevřeli, opustila popředí a Android pro ni přestal číst tagy. |
READER_LOST | Jen terminály Sunmi. Spojení s platební službou Sunmi se přerušilo. |
Jakmile relace skončí, každý další příkaz selže s SESSION_ENDED. Pro nové přiložení zavolejte znovu
openSession().
Zpracování chyb
Selhání při práci s tagem vyhazují TagSessionException, jejíž reason vám řekne, co dělat dál. Volání API dřív, než je
SDK nastavené, vyhodí NotInitialized. TagSessionException není AcceptException, proto je zachytávejte
zvlášť:
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
}Při nastaveném authLost se znovu autentizujte
U jakéhokoli důvodu může být nastaven příznak výjimky authLost. Pokud je nastaven, znovu se autentizujte, než pošlete
další příkaz do chráněné paměti; u DESFire navíc znovu vyberte aplikaci.
Pokud má argument klíče, bloku nebo stránky nesprávnou délku nebo znaménko, případně je rámec nebo APDU prázdné, vyhodí se
IllegalArgumentException dřív, než se cokoli dostane k tagu.
Otevírání relace
reason | Kdy nastává | Co dělat |
|---|---|---|
TIMEOUT | Během tapWait nedošlo k přiložení. | Znovu vyzvěte k přiložení, nebo pokus ukončete. |
READER_BUSY | Čtečku používá platba nebo jiná relace. | Zkuste to později; žádná fronta neexistuje. |
SESSION_ENDED | Během čekání na přiložení byla zahájena platba nebo aktivita opustila popředí. | Otevřete novou relaci, až platba skončí nebo bude aplikace znovu v popředí. |
NFC_DISABLED | Jen telefony. NFC je vypnuté v nastavení systému. | Požádejte uživatele, aby NFC zapnul. |
VENDOR_ERROR | Terminály Sunmi: platební služba není dostupná. vendorCode obsahuje její kód, pokud nějaký vrátila. | Zkuste to znovu. Pokud problém přetrvává, nahlaste jej podpoře pro vývojáře i s vendorCode. |
NOT_SUPPORTED | Zařízení nemá čtečku, kterou SDK dokáže použít. | Skryjte funkci, když isSupported() vrátí false. |
Odeslání příkazu
reason | Kdy nastává | Co dělat |
|---|---|---|
TIMEOUT | Tag neodpověděl včas; byl znovu vybrán. | Zkuste to znovu. Pokud je nastaven authLost, nejprve se autentizujte. |
NO_RESPONSE | Tag neodpověděl. | Zkuste to znovu, pokud je tag stále v poli čtečky. |
AUTH_FAILED | Nesprávný klíč nebo heslo. | Zkuste další klíč; tag už je znovu vybrán. |
NAK | Tag příkaz odmítl: stránka mimo rozsah, chráněná paměť nebo nepodporovaný příkaz. | Opravte příkaz. Relace pokračuje. |
NOT_SUPPORTED | Příkaz neodpovídá této rodině tagů nebo NFC řadiči tohoto telefonu (MIFARE Classic vyžaduje NXP). | Zkontrolujte session.info.family. Relace pokračuje. |
VENDOR_ERROR | Vlastní SDK čtečky ohlásilo poruchu; vendorCode obsahuje její kód. | Zkuste to znovu, poté poruchu nahlaste podpoře pro vývojáře i s vendorCode. |
Relace skončila
reason | Kdy nastává | Co dělat |
|---|---|---|
CARD_REMOVED | Tag opustil pole čtečky během příkazu. | Požádejte o nové přiložení a otevřete novou relaci. |
SESSION_ENDED | Příkaz byl odeslán po skončení relace. | Otevřete novou relaci. |
Časové limity
Chcete-li změnit výchozí hodnoty, předejte openSession() objekt TagTimeouts:
Accept.nfc.openSession(activity, TagTimeouts(tapWait = 15.seconds, idle = 30.seconds))| Časový limit | Výchozí | Maximum | Platí pro |
|---|---|---|---|
tapWait | 30 s | 60 s | Čekání na přiložení v openSession() |
operation | 1 s | 5 s | Každý příkaz na tagu |
reselect | 1 s | 5 s | Opětovný výběr tagu po neúspěšném příkazu |
idle | 10 s | 60 s | Doba mezi příkazy, po které relace skončí |
Každá hodnota musí být větší než nula. Vytvoření TagTimeouts s hodnotou mimo tyto rozsahy vyhodí
IllegalArgumentException.
Dobré vědět
- Jedna čtečka. Zařízení má jednu bezkontaktní čtečku, takže najednou může běžet jen jedna relace s tagem nebo
jedno čekání na přiložení. Druhé volání
openSession()selže sREADER_BUSY. Platby zahájené pomocíAccept.payments.pay()relaci nejprve ukončí; platby zahájené mimo SDK koordinovat nelze, proto relace udržujte krátké. - Zrušení příkazu jej nevrátí zpět. V okamžiku, kdy korutinu zrušíte, už příkaz dorazil k tagu
a stále se provádí; zahodí se jen jeho výsledek. Zrušený
writeBlocktedy může tag přesto změnit. - Příkazy se provádějí v pořadí. Metody relace lze volat z libovolné korutiny; k tagu se dostávají jedna po druhé, v pořadí, v jakém je voláte.
- Data tagů se nedostanou do logů. SDK nikdy neloguje klíče, hesla ani data tagů a UID maskuje (např.
04A1…E5F6). Jak vaše aplikace data tagů ukládá a zobrazuje, je na vás.
Otestování integrace
Na telefonu a, pokud je podporujete, na terminálu Sunmi řady P:
- Ověřte, že
isSupported()vracítruena obou zařízeních afalsena zařízení bez NFC. - Přiložte tag NTAG nebo Ultralight a ověřte, že relace hlásí správné UID a
readPages(0)vrací 16 bajtů. Poté proveďtewritePagea stránku znovu přečtěte. - Na kartě MIFARE Classic se autentizujte nesprávným klíčem a potvrďte
AUTH_FAILED, pak správným klíčem. - Zrušte
openSession()během čekání na přiložení a ihned otevřete novou relaci. - Během relace zahajte platbu: relace by měla skončit jako
PAYMENTa platba by měla projít. Během jiné relace tag oddalte: relace by měla skončit jakoCARD_REMOVED. - Na telefonu vypněte NFC a potvrďte
NFC_DISABLED. Během relace přesuňte aplikaci na pozadí a potvrďte, že relace skončí jakoACTIVITY_PAUSED.