Tapaya
Accettazione in presenzaGuida all'integrazioneTag NFCBeta

Tag NFC

AndroidBeta

Legga 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

DispositivoLettoreArtefatti necessari
Telefono o tablet Android con NFCNFC di Androidaccept-nfc
Terminale Sunmi della serie PIl lettore contactless del terminale, tramite il servizio di pagamento di Sunmiaccept-nfc e accept-nfc-sunmi
Qualsiasi altro dispositivoNessunoNon 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 tagEsempi
NFC Type 2NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C
MIFARE ClassicMIFARE Classic Mini, 1K, 4K
Smart card ISO-DEPMIFARE DESFire

Come funziona

  1. La Sua app chiama Accept.nfc.openSession(activity).
  2. L'SDK attiva il lettore del dispositivo e attende un avvicinamento.
  3. Il cliente avvicina un tag e openSession() restituisce una TagSession con l'UID e la famiglia del tag.
  4. La Sua app legge e scrive tramite la sessione, un comando alla volta.
  5. 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

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-nfc aggiunge l'autorizzazione NFC e contrassegna android.hardware.nfc come non obbligatorio, così Play mantiene la Sua app disponibile sui terminali Sunmi, che non dispongono dell'NFC di Android.
  • accept-nfc-sunmi aggiunge l'autorizzazione com.sunmi.perm.CONTACTLESS_CARD e una voce <queries> per il servizio di pagamento di Sunmi. Incorpora Sunmi PayLib.

Integrazione

Installazione

build.gradle.kts
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.nfc

Verificare 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:

CampoDescrizione
uidHexL'UID del tag in esadecimale maiuscolo, ad es. 04A1B2C3D4E5F6.
familyLa TagFamily, che indica quali operazioni sono applicabili.
sak, atqaHexIdentificazione di basso livello restituita al rilevamento del tag.
atsHexATS per i tag ISO_DEP; altrimenti null.
memoryBytesDimensione della memoria utente, se il lettore è in grado di determinarla; altrimenti null.
versionHexRisposta 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).

OperazioneCosa 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.

OperazioneCosa 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, restoreInseriscono 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.

OperazioneCosa 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 success

Chiudere 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
    }
}
TagSessionEndReasonQuando si verifica
CLOSEDLa Sua app ha chiamato close().
CARD_REMOVEDIl tag è uscito dal campo e non è rientrato entro il timeout reselect.
IDLE_TIMEOUTNessun 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_PAUSEDSolo telefoni. L'activity con cui ha aperto la sessione è uscita dal primo piano e Android ha smesso di leggere i tag per essa.
READER_LOSTSolo 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

reasonQuando si verificaCosa fare
TIMEOUTNessun avvicinamento entro tapWait.Ripeta l'invito, oppure rinunci.
READER_BUSYUn pagamento o un'altra sessione sta usando il lettore.Riprovi più tardi; non esiste una coda.
SESSION_ENDEDDurante 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_DISABLEDSolo telefoni. L'NFC è disattivato nelle impostazioni di sistema.Chieda all'utente di attivare l'NFC.
VENDOR_ERRORTerminali 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_SUPPORTEDIl dispositivo non dispone di un lettore che l'SDK possa usare.Nasconda la funzionalità quando isSupported() restituisce false.

Invio di un comando

reasonQuando si verificaCosa fare
TIMEOUTIl tag non ha risposto in tempo; è stato riselezionato.Riprovi. Se authLost è impostato, ripeta prima l'autenticazione.
NO_RESPONSEIl tag non ha risposto.Riprovi se il tag è ancora nel campo.
AUTH_FAILEDChiave o password errata.Provi la chiave successiva; il tag è già stato riselezionato.
NAKIl tag ha rifiutato il comando: pagina fuori intervallo, memoria protetta o comando non supportato.Corregga il comando. La sessione continua.
NOT_SUPPORTEDIl 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_ERRORL'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

reasonQuando si verificaCosa fare
CARD_REMOVEDIl 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))
TimeoutPredefinitoMassimoSi applica a
tapWait30 s60 sAttesa dell'avvicinamento in openSession()
operation1 s5 sOgni comando sul tag
reselect1 s5 sRiselezione del tag dopo un comando non riuscito
idle10 s60 sTempo 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'errore READER_BUSY. I pagamenti avviati con Accept.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 writeBlock annullato 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:

  1. Verifichi che isSupported() restituisca true su entrambi e false su un dispositivo senza NFC.
  2. Avvicini un tag NTAG o Ultralight e verifichi che la sessione riporti l'UID corretto e che readPages(0) restituisca 16 byte. Poi esegua writePage e rilegga la pagina.
  3. Su MIFARE Classic, esegua l'autenticazione con una chiave errata e verifichi AUTH_FAILED, poi con quella corretta.
  4. Annulli openSession() durante l'attesa dell'avvicinamento e apra subito una nuova sessione.
  5. Avvii un pagamento durante una sessione: la sessione dovrebbe chiudersi come PAYMENT e il pagamento andare a buon fine. Allontani il tag durante un'altra sessione: la sessione dovrebbe chiudersi come CARD_REMOVED.
  6. 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 come ACTIVITY_PAUSED.
Manca qualcosa in questa pagina?Richieda una funzionalità. Alleghiamo questa pagina, così il team vede il contesto.