Tag NFC
AndroidBetaLegga e scriva tag NFC dalla Sua app Android, usando l'NFC del telefono o il lettore di un terminale Sunmi della serie P.
Legga e scriva tag NFC, come carte fedeltà, badge di accesso, adesivi e smart card, direttamente dalla Sua app Android. L'SDK comunica con il lettore del dispositivo stesso: l'NFC di Android su un telefono, oppure il lettore contactless di un terminale Sunmi della serie P. Non c'è alcun plugin di mezzo.
Le sessioni con i tag sono distribuite in artefatti facoltativi, com.tapaya:accept-nfc e, per i terminali Sunmi,
com.tapaya:accept-nfc-sunmi. Li aggiunga solo se Le servono; i pagamenti con carta funzionano allo stesso modo con o senza di essi.
Dispositivi supportati
| Dispositivo | Lettore | Artefatti necessari |
|---|---|---|
| Telefono o tablet Android con NFC | NFC di Android | accept-nfc |
| Terminale Sunmi della serie P | Il lettore contactless del terminale, tramite il servizio di pagamento di Sunmi | accept-nfc e accept-nfc-sunmi |
| Qualsiasi altro dispositivo | Nessuno | Non supportato |
I terminali Sunmi della serie P non dispongono dell'NFC di Android; la loro antenna si trova dietro il modulo di pagamento. accept-nfc-sunmi
la raggiunge tramite il servizio di pagamento di Sunmi e subentra all'NFC di Android ogni volta che tale servizio è installato.
Su un dispositivo senza un lettore utilizzabile, isSupported() restituisce false e openSession() genera l'errore
NOT_SUPPORTED.
I telefoni hanno alcuni limiti aggiuntivi
I telefoni leggono i tag solo per l'activity in primo piano, quindi una sessione si chiude quando quell'activity esce dal primo piano.
MIFARE Classic funziona solo sui telefoni con un controller NFC NXP; altrove le sue operazioni generano l'errore
NOT_SUPPORTED. Per i tag ISO-DEP, atsHex contiene solo i byte storici (historical bytes) dell'ATS, perché Android non espone
il resto.
Tag supportati
| Tipo di tag | Esempi |
|---|---|
| NFC Type 2 | NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C |
| MIFARE Classic | MIFARE Classic Mini, 1K, 4K |
| Smart card ISO-DEP | MIFARE DESFire |
Come funziona
- La Sua app chiama
Accept.nfc.openSession(activity). - L'SDK attiva il lettore del dispositivo e attende un avvicinamento.
- Il cliente avvicina un tag e
openSession()restituisce unaTagSessioncon l'UID e la famiglia del tag. - La Sua app legge e scrive tramite la sessione, un comando alla volta.
- La Sua app chiama
close(), che libera il lettore per i pagamenti e per altre sessioni con i tag.
I pagamenti hanno sempre la priorità
Un dispositivo ha un solo lettore contactless, che una sessione con il tag e un pagamento con carta non possono condividere. L'avvio di un pagamento
con Accept.payments.pay() chiude la Sua sessione, e openSession() genera l'errore READER_BUSY finché il pagamento non è
terminato. Sia pronto alla chiusura di una sessione in qualsiasi momento; si veda Chiudere una sessione.
Prerequisiti
- Un dispositivo supportato.
- L'Accept SDK installato e inizializzato. Chiamare
isSupported()oopenSession()prima diAccept.initializelanciaNotInitialized.
Le sessioni con i tag non richiedono che un esercente abbia effettuato l'accesso, né il plugin Tapaya Terminal, né l'autorizzazione alla posizione. Gli artefatti aggiungono al manifest ciò di cui hanno bisogno:
accept-nfcaggiunge l'autorizzazioneNFCe contrassegnaandroid.hardware.nfccome non obbligatorio, così Play mantiene la Sua app disponibile sui terminali Sunmi, che non dispongono dell'NFC di Android.accept-nfc-sunmiaggiunge l'autorizzazionecom.sunmi.perm.CONTACTLESS_CARDe una voce<queries>per il servizio di pagamento di Sunmi. Incorpora Sunmi PayLib.
Integrazione
Installazione
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")
}Se dichiara anche com.tapaya:accept, mantenga tutti gli artefatti alla stessa versione.
Accept.nfc è una proprietà di estensione (extension property); la importi prima di usarla:
import com.tapaya.accept.nfc.nfcVerificare il supporto
Chiami isSupported() per decidere se mostrare o meno la funzionalità dei tag:
if (Accept.nfc.isSupported()) {
showScanTagButton()
}isSupported() non richiede alcun avvicinamento e non interagisce con il lettore, quindi può essere chiamato in sicurezza in qualsiasi momento. Indica se il dispositivo
dispone di un lettore che l'SDK può usare. Restituisce true su un telefono con l'NFC disattivato; in tal caso openSession() genera
l'errore NFC_DISABLED. Lancia NotInitialized se Accept.initialize non è ancora stato eseguito.
Aprire una sessione
Passi l'activity che si trova in primo piano ed è nello stato 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() resta sospeso finché non viene avvicinato un tag o finché non scade tapWait (30 secondi per impostazione predefinita). Durante l'attesa
mostri un invito come “Avvicini la carta”. Lancia IllegalStateException se activity non è nello stato resumed.
Su un telefono l'activity è importante: Android legge i tag solo per essa, e l'uscita dal primo piano interrompe l'attesa o chiude
la sessione con ACTIVITY_PAUSED. I terminali Sunmi leggono i tag indipendentemente da ciò che è in primo piano, ma l'activity va
passata comunque.
Per interrompere l'attesa, ad esempio quando il cliente tocca Cancel, annulli la coroutine che ha chiamato openSession().
Il lettore smette di attendere e Lei può aprire subito una nuova sessione.
Leggere le informazioni della sessione
session.info descrive il tag avvicinato:
| Campo | Descrizione |
|---|---|
uidHex | L'UID del tag in esadecimale maiuscolo, ad es. 04A1B2C3D4E5F6. |
family | La TagFamily, che indica quali operazioni sono applicabili. |
sak, atqaHex | Identificazione di basso livello restituita al rilevamento del tag. |
atsHex | ATS per i tag ISO_DEP; altrimenti null. |
memoryBytes | Dimensione della memoria utente, se il lettore è in grado di determinarla; altrimenti null. |
versionHex | Risposta a GET_VERSION, per i tag che lo supportano; altrimenti null. |
Leggere e scrivere un tag
Scelga il ramo in base a session.info.family, quindi usi le operazioni previste per quel tipo di tag:
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.")
}Un comando non adatto al tag avvicinato genera l'errore NOT_SUPPORTED, e la sessione resta aperta.
NFC Type 2
NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C e altri tag Type 2 (TYPE2_OTHER).
| Operazione | Cosa fa |
|---|---|
readPages(startPage) | Restituisce 16 byte: quattro pagine da 4 byte a partire da startPage. |
writePage(page, data) | Scrive una pagina da 4 byte. |
passwordAuth(password) | Solo NTAG e Ultralight EV1. Sblocca le pagine protette con una password di 4 byte e restituisce il PACK di 2 byte, che può usare per verificare che il tag sia autentico. |
ultralightCAuth(key) | Solo Ultralight C. Esegue l'autenticazione con una chiave 3DES di 16 byte. |
transceiveType2(frame) | Invia un frame Type 2 grezzo, per i comandi senza un metodo dedicato, e restituisce la risposta del tag. |
// 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 e 4K. Autentichi un settore prima di leggerne o scriverne i blocchi.
| Operazione | Cosa fa |
|---|---|
authenticateClassic(block, keyType, key) | Autentica il settore che contiene block con una chiave di 6 byte. |
readBlock(block) | Restituisce i 16 byte del blocco. |
writeBlock(block, data) | Scrive 16 byte nel blocco. |
increment, decrement, restore | Inseriscono il risultato di un'operazione su un value block nel transfer buffer del tag. |
transfer(block) | Scrive il transfer buffer in un blocco. |
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)Una chiave errata genera l'errore AUTH_FAILED e l'SDK riseleziona il tag automaticamente, così può provare subito la chiave
successiva:
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")Smart card ISO-DEP
MIFARE DESFire e altre carte ISO 14443-4.
| Operazione | Cosa fa |
|---|---|
transmitApdu(apdu) | Invia un'APDU di comando e restituisce la risposta: i dati seguiti da 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 successChiudere una sessione
Chiami close() non appena ha terminato, idealmente da un blocco finally. Finché non lo fa, nessun pagamento può usare il
lettore. close() può essere chiamato in sicurezza più di una volta e non lancia mai eccezioni per una sessione già chiusa.
Una sessione può anche chiudersi da sola. session.ended è uno StateFlow che resta null finché la sessione è attiva,
poi emette il motivo della chiusura:
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 | Quando si verifica |
|---|---|
CLOSED | La Sua app ha chiamato close(). |
CARD_REMOVED | Il tag è uscito dal campo e non è rientrato entro il timeout reselect. |
IDLE_TIMEOUT | Nessun comando è arrivato entro il timeout idle (10 secondi per impostazione predefinita). |
PAYMENT | È iniziato un pagamento tramite Accept.payments.pay(). I pagamenti hanno sempre la priorità. |
ACTIVITY_PAUSED | Solo telefoni. L'activity con cui ha aperto la sessione è uscita dal primo piano e Android ha smesso di leggere i tag per essa. |
READER_LOST | Solo terminali Sunmi. La connessione al servizio di pagamento di Sunmi si è interrotta. |
Una volta chiusa una sessione, ogni comando successivo genera l'errore SESSION_ENDED. Chiami di nuovo openSession() per un nuovo
avvicinamento.
Gestione degli errori
Gli errori relativi ai tag lanciano TagSessionException, il cui reason indica cosa fare dopo. Chiamare l'API prima che
l'SDK sia configurato lancia NotInitialized. TagSessionException non è un'AcceptException, quindi intercetti le due
separatamente:
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
}Ripeta l'autenticazione quando authLost è impostato
Con qualsiasi motivo, il flag authLost dell'eccezione può essere impostato. In tal caso, ripeta l'autenticazione prima di inviare
un altro comando alla memoria protetta; su DESFire, riselezioni anche l'applicazione.
Un argomento chiave, blocco o pagina di lunghezza o segno errati, oppure un frame o un'APDU vuoti, lancia
IllegalArgumentException prima che qualcosa raggiunga il tag.
Apertura di una sessione
reason | Quando si verifica | Cosa fare |
|---|---|---|
TIMEOUT | Nessun avvicinamento entro tapWait. | Ripeta l'invito, oppure rinunci. |
READER_BUSY | Un pagamento o un'altra sessione sta usando il lettore. | Riprovi più tardi; non esiste una coda. |
SESSION_ENDED | Durante l'attesa dell'avvicinamento è iniziato un pagamento, oppure l'activity è uscita dal primo piano. | Apra una nuova sessione quando il pagamento è terminato o l'app è tornata in primo piano. |
NFC_DISABLED | Solo telefoni. L'NFC è disattivato nelle impostazioni di sistema. | Chieda all'utente di attivare l'NFC. |
VENDOR_ERROR | Terminali Sunmi: il servizio di pagamento non è disponibile. vendorCode contiene il codice restituito dal servizio, se ne ha fornito uno. | Riprovi. Se il problema persiste, lo segnali all'assistenza per sviluppatori indicando vendorCode. |
NOT_SUPPORTED | Il dispositivo non dispone di un lettore che l'SDK possa usare. | Nasconda la funzionalità quando isSupported() restituisce false. |
Invio di un comando
reason | Quando si verifica | Cosa fare |
|---|---|---|
TIMEOUT | Il tag non ha risposto in tempo; è stato riselezionato. | Riprovi. Se authLost è impostato, ripeta prima l'autenticazione. |
NO_RESPONSE | Il tag non ha risposto. | Riprovi se il tag è ancora nel campo. |
AUTH_FAILED | Chiave o password errata. | Provi la chiave successiva; il tag è già stato riselezionato. |
NAK | Il tag ha rifiutato il comando: pagina fuori intervallo, memoria protetta o comando non supportato. | Corregga il comando. La sessione continua. |
NOT_SUPPORTED | Il comando non è adatto a questa famiglia di tag, o al controller NFC di questo telefono (MIFARE Classic richiede NXP). | Verifichi session.info.family. La sessione continua. |
VENDOR_ERROR | L'SDK del lettore stesso ha segnalato un guasto; vendorCode ne contiene il codice. | Riprovi, poi lo segnali all'assistenza per sviluppatori indicando vendorCode. |
Sessione terminata
reason | Quando si verifica | Cosa fare |
|---|---|---|
CARD_REMOVED | Il tag è uscito dal campo durante il comando. | Chieda di nuovo di avvicinare il tag e apra una nuova sessione. |
SESSION_ENDED | È stato inviato un comando dopo la chiusura della sessione. | Apra una nuova sessione. |
Timeout
Per modificare i valori predefiniti, passi TagTimeouts a openSession():
Accept.nfc.openSession(activity, TagTimeouts(tapWait = 15.seconds, idle = 30.seconds))| Timeout | Predefinito | Massimo | Si applica a |
|---|---|---|---|
tapWait | 30 s | 60 s | Attesa dell'avvicinamento in openSession() |
operation | 1 s | 5 s | Ogni comando sul tag |
reselect | 1 s | 5 s | Riselezione del tag dopo un comando non riuscito |
idle | 10 s | 60 s | Tempo tra un comando e l'altro prima che la sessione si chiuda |
Ogni valore deve essere maggiore di zero. Creare TagTimeouts con un valore al di fuori di questi intervalli lancia
IllegalArgumentException.
Da sapere
- Un solo lettore. Il dispositivo ha un solo lettore contactless, quindi è possibile una sola sessione con il tag o una sola attesa di avvicinamento alla volta. Un secondo
openSession()genera l'erroreREADER_BUSY. I pagamenti avviati conAccept.payments.pay()chiudono prima la sessione; i pagamenti avviati al di fuori dell'SDK non possono essere coordinati, quindi mantenga le sessioni brevi. - Annullare un comando non lo annulla sul tag. Nel momento in cui annulla la coroutine, il comando ha già raggiunto il tag
e viene comunque eseguito; viene scartato solo il suo risultato. Un
writeBlockannullato può comunque modificare il tag. - I comandi vengono eseguiti in ordine. I metodi della sessione possono essere chiamati da qualsiasi coroutine; raggiungono il tag uno alla volta, nell'ordine in cui li chiama.
- I dati dei tag restano fuori dai log. L'SDK non registra mai nei log chiavi, password o dati dei tag e maschera gli UID (ad es.
04A1…E5F6). Il modo in cui la Sua app salva e mostra i dati dei tag dipende da Lei.
Testare l'integrazione
Su un telefono e, se supporta i terminali Sunmi della serie P, anche su uno di questi:
- Verifichi che
isSupported()restituiscatruesu entrambi efalsesu un dispositivo senza NFC. - Avvicini un tag NTAG o Ultralight e verifichi che la sessione riporti l'UID corretto e che
readPages(0)restituisca 16 byte. Poi eseguawritePagee rilegga la pagina. - Su MIFARE Classic, esegua l'autenticazione con una chiave errata e verifichi
AUTH_FAILED, poi con quella corretta. - Annulli
openSession()durante l'attesa dell'avvicinamento e apra subito una nuova sessione. - Avvii un pagamento durante una sessione: la sessione dovrebbe chiudersi come
PAYMENTe il pagamento andare a buon fine. Allontani il tag durante un'altra sessione: la sessione dovrebbe chiudersi comeCARD_REMOVED. - Su un telefono, disattivi l'NFC e verifichi
NFC_DISABLED. Durante una sessione, porti l'app in background e verifichi che la sessione si chiuda comeACTIVITY_PAUSED.