NFC-Tags
AndroidBetaLesen und beschreiben Sie NFC-Tags in Ihrer Android-App, über das NFC des Telefons oder das Lesegerät eines Terminals der Sunmi-P-Serie.
Lesen und beschreiben Sie NFC-Tags wie Kundenkarten, Zutrittsausweise, Sticker und Chipkarten direkt in Ihrer Android-App. Das SDK kommuniziert mit dem geräteeigenen Lesegerät: mit Android NFC auf einem Telefon oder mit dem kontaktlosen Lesegerät eines Terminals der Sunmi-P-Serie. Dazwischen ist kein Plugin geschaltet.
Tag-Sessions werden in optionalen Artefakten ausgeliefert: com.tapaya:accept-nfc und, für Sunmi-Terminals,
com.tapaya:accept-nfc-sunmi. Fügen Sie sie nur hinzu, wenn Sie sie benötigen; Kartenzahlungen funktionieren mit und ohne sie gleich.
Unterstützte Geräte
| Gerät | Lesegerät | Benötigte Artefakte |
|---|---|---|
| Android-Telefon oder -Tablet mit NFC | Android NFC | accept-nfc |
| Terminal der Sunmi-P-Serie | Das kontaktlose Lesegerät des Terminals, über den Zahlungsdienst (Pay Service) von Sunmi | accept-nfc und accept-nfc-sunmi |
| Jedes andere Gerät | Keines | Nicht unterstützt |
Terminals der Sunmi-P-Serie haben kein Android NFC; ihre Antenne sitzt hinter dem Zahlungsmodul. accept-nfc-sunmi
erreicht sie über den Zahlungsdienst von Sunmi und tritt an die Stelle von Android NFC, wann immer dieser Dienst installiert ist.
Auf einem Gerät ohne nutzbares Lesegerät gibt isSupported() den Wert false zurück, und openSession() schlägt mit
NOT_SUPPORTED fehl.
Für Telefone gelten einige zusätzliche Einschränkungen
Telefone lesen Tags nur für die Activity im Vordergrund; eine Session endet daher, sobald diese Activity den Vordergrund verlässt.
MIFARE Classic funktioniert nur auf Telefonen mit einem NFC-Controller von NXP; auf anderen Telefonen schlagen die zugehörigen Operationen mit
NOT_SUPPORTED fehl. Bei ISO-DEP-Tags enthält atsHex nur die historischen Bytes des ATS, weil Android den Rest
nicht bereitstellt.
Unterstützte Tags
| Tag-Typ | Beispiele |
|---|---|
| NFC Type 2 | NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C |
| MIFARE Classic | MIFARE Classic Mini, 1K, 4K |
| ISO-DEP-Chipkarten | MIFARE DESFire |
So funktioniert es
- Ihre App ruft
Accept.nfc.openSession(activity)auf. - Das SDK schaltet das Lesegerät des Geräts ein und wartet darauf, dass ein Tag vorgehalten wird.
- Der Kunde hält einen Tag vor, und
openSession()gibt eineTagSessionmit der UID und der Familie des Tags zurück. - Ihre App liest und schreibt über die Session, jeweils einen Befehl nach dem anderen.
- Ihre App ruft
close()auf und gibt damit das Lesegerät für Zahlungen und andere Tag-Sessions frei.
Zahlungen haben immer Vorrang
Ein Gerät hat ein einziges kontaktloses Lesegerät, das sich eine Tag-Session und eine Kartenzahlung nicht teilen können. Der Start einer Zahlung
mit Accept.payments.pay() beendet Ihre Session, und openSession() schlägt mit READER_BUSY fehl, bis die Zahlung
abgeschlossen ist. Rechnen Sie damit, dass eine Session jederzeit enden kann; siehe Session beenden.
Voraussetzungen
- Ein unterstütztes Gerät.
- Das Accept SDK ist installiert und initialisiert. Wenn Sie
isSupported()oderopenSession()vorAccept.initializeaufrufen, wirdNotInitializedgeworfen.
Tag-Sessions benötigen keinen angemeldeten Händler, kein Tapaya-Terminal-Plugin und keine Standortberechtigung. Die Artefakte ergänzen Ihr Manifest um alles, was sie benötigen:
accept-nfcfügt die BerechtigungNFChinzu und kennzeichnetandroid.hardware.nfcals nicht erforderlich, sodass Google Play Ihre App auch auf Sunmi-Terminals verfügbar hält, die kein Android NFC haben.accept-nfc-sunmifügt die Berechtigungcom.sunmi.perm.CONTACTLESS_CARDund einen<queries>-Eintrag für den Zahlungsdienst von Sunmi hinzu. Es bettet Sunmi PayLib ein.
Integration
Installieren
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")
}Wenn Sie zusätzlich com.tapaya:accept deklarieren, halten Sie alle Artefakte auf derselben Version.
Accept.nfc ist eine Extension Property; importieren Sie sie vor der Verwendung:
import com.tapaya.accept.nfc.nfcUnterstützung prüfen
Rufen Sie isSupported() auf, um zu entscheiden, ob Sie die Tag-Funktion überhaupt anzeigen:
if (Accept.nfc.isSupported()) {
showScanTagButton()
}isSupported() erfordert kein Vorhalten und greift nicht auf das Lesegerät zu. Sie können die Methode daher jederzeit gefahrlos aufrufen. Sie sagt Ihnen, ob das Gerät
ein Lesegerät hat, das das SDK nutzen kann. Auf einem Telefon mit ausgeschaltetem NFC gibt sie true zurück; openSession() schlägt dann
mit NFC_DISABLED fehl. Wenn Accept.initialize noch nicht ausgeführt wurde, wirft sie NotInitialized.
Session öffnen
Übergeben Sie die Activity, die sich im Vordergrund und im Zustand „resumed“ befindet:
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() suspendiert, bis ein Tag vorgehalten wird oder tapWait abläuft (standardmäßig 30 Sekunden). Zeigen Sie während des Wartens
die Aufforderung „Karte vorhalten“ an. Befindet sich activity nicht im Zustand „resumed“, wird eine IllegalStateException geworfen.
Auf einem Telefon spielt die Activity eine Rolle: Android liest Tags nur für sie, und verlässt sie den Vordergrund, wird das Warten abgebrochen
oder die Session mit ACTIVITY_PAUSED beendet. Sunmi-Terminals lesen Tags unabhängig davon, was sich im Vordergrund befindet, Sie übergeben aber
trotzdem eine Activity.
Um das Warten abzubrechen, zum Beispiel wenn der Kunde auf Cancel tippt, brechen Sie die Coroutine ab, die openSession() aufgerufen hat.
Das Lesegerät hört auf zu warten, und Sie können sofort eine neue Session öffnen.
Session-Informationen auslesen
session.info beschreibt den vorgehaltenen Tag:
| Feld | Beschreibung |
|---|---|
uidHex | Die UID des Tags als Hexadezimalwert in Großbuchstaben, z. B. 04A1B2C3D4E5F6. |
family | Die TagFamily, an der Sie erkennen, welche Operationen anwendbar sind. |
sak, atqaHex | Low-Level-Identifikation, die beim Erkennen des Tags zurückgegeben wird. |
atsHex | ATS für Tags vom Typ ISO_DEP; sonst null. |
memoryBytes | Größe des Benutzerspeichers, sofern das Lesegerät sie ermitteln kann; sonst null. |
versionHex | Antwort auf GET_VERSION bei Tags, die diesen Befehl unterstützen; sonst null. |
Tag lesen und beschreiben
Verzweigen Sie anhand von session.info.family und verwenden Sie dann die Operationen für diesen Tag-Typ:
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.")
}Ein Befehl, der nicht zum vorgehaltenen Tag passt, schlägt mit NOT_SUPPORTED fehl, und die Session bleibt geöffnet.
NFC Type 2
NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C und andere Type-2-Tags (TYPE2_OTHER).
| Operation | Funktion |
|---|---|
readPages(startPage) | Gibt 16 Byte zurück: vier 4-Byte-Seiten ab startPage. |
writePage(page, data) | Schreibt eine 4-Byte-Seite. |
passwordAuth(password) | Nur NTAG und Ultralight EV1. Entsperrt geschützte Seiten mit einem 4-Byte-Passwort und gibt das 2-Byte-PACK zurück, mit dem Sie prüfen können, ob der Tag echt ist. |
ultralightCAuth(key) | Nur Ultralight C. Authentifiziert mit einem 16-Byte-3DES-Schlüssel. |
transceiveType2(frame) | Sendet einen rohen Type-2-Frame für Befehle ohne eigene Methode und gibt die Antwort des Tags zurück. |
// 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 und 4K. Authentifizieren Sie einen Sektor, bevor Sie seine Blöcke lesen oder beschreiben.
| Operation | Funktion |
|---|---|
authenticateClassic(block, keyType, key) | Authentifiziert den Sektor, der block enthält, mit einem 6-Byte-Schlüssel. |
readBlock(block) | Gibt die 16 Byte des Blocks zurück. |
writeBlock(block, data) | Schreibt 16 Byte in den Block. |
increment, decrement, restore | Legen das Ergebnis einer Value-Block-Operation im Transferpuffer des Tags ab. |
transfer(block) | Schreibt den Transferpuffer in einen 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)Ein falscher Schlüssel schlägt mit AUTH_FAILED fehl, und das SDK wählt den Tag für Sie erneut aus (Re-Select), sodass Sie sofort den nächsten Schlüssel
versuchen können:
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-Chipkarten
MIFARE DESFire und andere Karten nach ISO 14443-4.
| Operation | Funktion |
|---|---|
transmitApdu(apdu) | Sendet eine Befehls-APDU und gibt die Antwort zurück: die Daten, gefolgt von 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 successSession beenden
Rufen Sie close() auf, sobald Sie fertig sind, idealerweise aus einem finally-Block. Bis dahin kann keine Zahlung das
Lesegerät nutzen. close() kann gefahrlos mehrfach aufgerufen werden und wirft bei einer bereits beendeten Session nie eine Exception.
Eine Session kann auch von selbst enden. session.ended ist ein StateFlow, der null bleibt, solange die Session aktiv ist,
und dann den Grund ausgibt, aus dem sie geendet hat:
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 | Wann es eintritt |
|---|---|
CLOSED | Ihre App hat close() aufgerufen. |
CARD_REMOVED | Der Tag hat das Feld verlassen und ist nicht innerhalb des Timeouts reselect zurückgekehrt. |
IDLE_TIMEOUT | Innerhalb des Timeouts idle (standardmäßig 10 Sekunden) kam kein Befehl. |
PAYMENT | Eine Zahlung wurde über Accept.payments.pay() gestartet. Zahlungen haben immer Vorrang. |
ACTIVITY_PAUSED | Nur auf Telefonen. Die Activity, mit der Sie die Session geöffnet haben, hat den Vordergrund verlassen, und Android liest keine Tags mehr für sie. |
READER_LOST | Nur auf Sunmi-Terminals. Die Verbindung zum Zahlungsdienst von Sunmi ist abgebrochen. |
Sobald eine Session beendet ist, schlägt jeder weitere Befehl mit SESSION_ENDED fehl. Rufen Sie für ein neues Vorhalten
erneut openSession() auf.
Fehlerbehandlung
Bei Fehlern am Tag wird eine TagSessionException geworfen, deren reason Ihnen sagt, was als Nächstes zu tun ist. Wird die API aufgerufen, bevor das
SDK eingerichtet ist, wird NotInitialized geworfen. TagSessionException ist keine AcceptException, fangen Sie die beiden daher getrennt ab:
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
}Bei gesetztem authLost erneut authentifizieren
Bei jedem Grund kann das Flag authLost der Exception gesetzt sein. Ist es gesetzt, authentifizieren Sie sich erneut, bevor Sie
einen weiteren Befehl an geschützten Speicher senden; bei DESFire wählen Sie zusätzlich die Anwendung erneut aus.
Ein Schlüssel-, Block- oder Seitenargument mit falscher Länge oder falschem Vorzeichen oder ein leerer Frame bzw. eine leere APDU wirft
eine IllegalArgumentException, bevor irgendetwas den Tag erreicht.
Beim Öffnen einer Session
reason | Wann es eintritt | Was zu tun ist |
|---|---|---|
TIMEOUT | Innerhalb von tapWait wurde kein Tag vorgehalten. | Fordern Sie erneut zum Vorhalten auf oder brechen Sie ab. |
READER_BUSY | Eine Zahlung oder eine andere Session nutzt das Lesegerät. | Versuchen Sie es später erneut; es gibt keine Warteschlange. |
SESSION_ENDED | Während des Wartens auf das Vorhalten wurde eine Zahlung gestartet, oder die Activity hat den Vordergrund verlassen. | Öffnen Sie eine neue Session, sobald die Zahlung abgeschlossen ist oder die App wieder im Vordergrund ist. |
NFC_DISABLED | Nur auf Telefonen. NFC ist in den Systemeinstellungen ausgeschaltet. | Bitten Sie den Nutzer, NFC einzuschalten. |
VENDOR_ERROR | Sunmi-Terminals: Der Zahlungsdienst ist nicht verfügbar. vendorCode enthält dessen Code, sofern er einen geliefert hat. | Versuchen Sie es erneut. Besteht der Fehler weiter, melden Sie ihn mit vendorCode dem Entwickler-Support. |
NOT_SUPPORTED | Das Gerät hat kein Lesegerät, das das SDK nutzen kann. | Blenden Sie die Funktion aus, wenn isSupported() den Wert false zurückgibt. |
Beim Senden eines Befehls
reason | Wann es eintritt | Was zu tun ist |
|---|---|---|
TIMEOUT | Der Tag hat nicht rechtzeitig geantwortet; er wurde erneut ausgewählt. | Versuchen Sie es erneut. Ist authLost gesetzt, authentifizieren Sie sich zuerst. |
NO_RESPONSE | Der Tag hat nicht geantwortet. | Versuchen Sie es erneut, wenn sich der Tag noch im Feld befindet. |
AUTH_FAILED | Falscher Schlüssel oder falsches Passwort. | Versuchen Sie den nächsten Schlüssel; der Tag ist bereits erneut ausgewählt. |
NAK | Der Tag hat den Befehl abgelehnt: Seite außerhalb des Bereichs, geschützter Speicher oder nicht unterstützter Befehl. | Korrigieren Sie den Befehl. Die Session läuft weiter. |
NOT_SUPPORTED | Der Befehl passt nicht zu dieser Tag-Familie oder zum NFC-Controller dieses Telefons (MIFARE Classic erfordert NXP). | Prüfen Sie session.info.family. Die Session läuft weiter. |
VENDOR_ERROR | Das SDK des Lesegeräts selbst hat eine Störung gemeldet; vendorCode enthält deren Code. | Versuchen Sie es erneut und melden Sie sie dann mit vendorCode dem Entwickler-Support. |
Session beendet
reason | Wann es eintritt | Was zu tun ist |
|---|---|---|
CARD_REMOVED | Der Tag hat das Feld während des Befehls verlassen. | Fordern Sie erneut zum Vorhalten auf und öffnen Sie eine neue Session. |
SESSION_ENDED | Ein Befehl wurde nach dem Ende der Session gesendet. | Öffnen Sie eine neue Session. |
Timeouts
Übergeben Sie TagTimeouts an openSession(), um die Standardwerte zu ändern:
Accept.nfc.openSession(activity, TagTimeouts(tapWait = 15.seconds, idle = 30.seconds))| Timeout | Standard | Maximum | Gilt für |
|---|---|---|---|
tapWait | 30 s | 60 s | Warten auf das Vorhalten in openSession() |
operation | 1 s | 5 s | Jeden Befehl auf dem Tag |
reselect | 1 s | 5 s | Erneute Auswahl des Tags nach einem fehlgeschlagenen Befehl |
idle | 10 s | 60 s | Zeit zwischen Befehlen, nach der die Session endet |
Jeder Wert muss größer als null sein. Wird TagTimeouts mit einem Wert außerhalb dieser Bereiche erstellt, wird
eine IllegalArgumentException geworfen.
Gut zu wissen
- Ein einziges Lesegerät. Das Gerät hat ein einziges kontaktloses Lesegerät, daher ist jeweils nur eine Tag-Session oder ein ausstehendes Warten auf das Vorhalten möglich. Ein zweiter
Aufruf von
openSession()schlägt mitREADER_BUSYfehl. Zahlungen, die mitAccept.payments.pay()gestartet werden, beenden zuerst eine Session; Zahlungen, die außerhalb des SDK gestartet werden, lassen sich nicht koordinieren, halten Sie Sessions daher kurz. - Das Abbrechen eines Befehls macht ihn nicht rückgängig. Wenn Sie die Coroutine abbrechen, hat der Befehl den Tag bereits erreicht
und wird weiter ausgeführt; nur sein Ergebnis wird verworfen. Ein abgebrochenes
writeBlockkann den Tag also trotzdem verändern. - Befehle werden der Reihe nach ausgeführt. Session-Methoden können aus jeder Coroutine aufgerufen werden; sie erreichen den Tag einzeln, in der Reihenfolge Ihrer Aufrufe.
- Tag-Daten gelangen nicht in Logs. Das SDK protokolliert nie Schlüssel, Passwörter oder Tag-Daten und maskiert UIDs (z. B.
04A1…E5F6). Wie Ihre App Tag-Daten speichert und anzeigt, liegt bei Ihnen.
Integration testen
Auf einem Telefon und, falls Sie diese unterstützen, auf einem Terminal der Sunmi-P-Serie:
- Prüfen Sie, dass
isSupported()auf beidentrueund auf einem Gerät ohne NFCfalsezurückgibt. - Halten Sie einen NTAG- oder Ultralight-Tag vor und prüfen Sie, dass die Session die richtige UID meldet und
readPages(0)16 Byte zurückgibt. Führen Sie dannwritePageaus und lesen Sie die Seite erneut. - Authentifizieren Sie sich bei MIFARE Classic mit einem falschen Schlüssel und bestätigen Sie
AUTH_FAILED, dann mit dem richtigen. - Brechen Sie
openSession()während des Wartens auf das Vorhalten ab und öffnen Sie sofort eine neue Session. - Starten Sie während einer Session eine Zahlung: Die Session sollte als
PAYMENTenden und die Zahlung durchgehen. Entfernen Sie während einer anderen Session den Tag: Sie sollte alsCARD_REMOVEDenden. - Schalten Sie auf einem Telefon NFC aus und bestätigen Sie
NFC_DISABLED. Schicken Sie die App während einer Session in den Hintergrund und bestätigen Sie, dass die Session alsACTIVITY_PAUSEDendet.