Tapaya
Zahlungsannahme vor OrtIntegrationsleitfadenNFC-TagsBeta

NFC-Tags

AndroidBeta

Lesen 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ätLesegerätBenötigte Artefakte
Android-Telefon oder -Tablet mit NFCAndroid NFCaccept-nfc
Terminal der Sunmi-P-SerieDas kontaktlose Lesegerät des Terminals, über den Zahlungsdienst (Pay Service) von Sunmiaccept-nfc und accept-nfc-sunmi
Jedes andere GerätKeinesNicht 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-TypBeispiele
NFC Type 2NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C
MIFARE ClassicMIFARE Classic Mini, 1K, 4K
ISO-DEP-ChipkartenMIFARE DESFire

So funktioniert es

  1. Ihre App ruft Accept.nfc.openSession(activity) auf.
  2. Das SDK schaltet das Lesegerät des Geräts ein und wartet darauf, dass ein Tag vorgehalten wird.
  3. Der Kunde hält einen Tag vor, und openSession() gibt eine TagSession mit der UID und der Familie des Tags zurück.
  4. Ihre App liest und schreibt über die Session, jeweils einen Befehl nach dem anderen.
  5. 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

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-nfc fügt die Berechtigung NFC hinzu und kennzeichnet android.hardware.nfc als nicht erforderlich, sodass Google Play Ihre App auch auf Sunmi-Terminals verfügbar hält, die kein Android NFC haben.
  • accept-nfc-sunmi fügt die Berechtigung com.sunmi.perm.CONTACTLESS_CARD und einen <queries>-Eintrag für den Zahlungsdienst von Sunmi hinzu. Es bettet Sunmi PayLib ein.

Integration

Installieren

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")
}

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

Unterstü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:

FeldBeschreibung
uidHexDie UID des Tags als Hexadezimalwert in Großbuchstaben, z. B. 04A1B2C3D4E5F6.
familyDie TagFamily, an der Sie erkennen, welche Operationen anwendbar sind.
sak, atqaHexLow-Level-Identifikation, die beim Erkennen des Tags zurückgegeben wird.
atsHexATS für Tags vom Typ ISO_DEP; sonst null.
memoryBytesGröße des Benutzerspeichers, sofern das Lesegerät sie ermitteln kann; sonst null.
versionHexAntwort 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).

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

OperationFunktion
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, restoreLegen 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.

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

Session 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
    }
}
TagSessionEndReasonWann es eintritt
CLOSEDIhre App hat close() aufgerufen.
CARD_REMOVEDDer Tag hat das Feld verlassen und ist nicht innerhalb des Timeouts reselect zurückgekehrt.
IDLE_TIMEOUTInnerhalb des Timeouts idle (standardmäßig 10 Sekunden) kam kein Befehl.
PAYMENTEine Zahlung wurde über Accept.payments.pay() gestartet. Zahlungen haben immer Vorrang.
ACTIVITY_PAUSEDNur 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_LOSTNur 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

reasonWann es eintrittWas zu tun ist
TIMEOUTInnerhalb von tapWait wurde kein Tag vorgehalten.Fordern Sie erneut zum Vorhalten auf oder brechen Sie ab.
READER_BUSYEine Zahlung oder eine andere Session nutzt das Lesegerät.Versuchen Sie es später erneut; es gibt keine Warteschlange.
SESSION_ENDEDWä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_DISABLEDNur auf Telefonen. NFC ist in den Systemeinstellungen ausgeschaltet.Bitten Sie den Nutzer, NFC einzuschalten.
VENDOR_ERRORSunmi-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_SUPPORTEDDas 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

reasonWann es eintrittWas zu tun ist
TIMEOUTDer Tag hat nicht rechtzeitig geantwortet; er wurde erneut ausgewählt.Versuchen Sie es erneut. Ist authLost gesetzt, authentifizieren Sie sich zuerst.
NO_RESPONSEDer Tag hat nicht geantwortet.Versuchen Sie es erneut, wenn sich der Tag noch im Feld befindet.
AUTH_FAILEDFalscher Schlüssel oder falsches Passwort.Versuchen Sie den nächsten Schlüssel; der Tag ist bereits erneut ausgewählt.
NAKDer 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_SUPPORTEDDer 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_ERRORDas 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

reasonWann es eintrittWas zu tun ist
CARD_REMOVEDDer Tag hat das Feld während des Befehls verlassen.Fordern Sie erneut zum Vorhalten auf und öffnen Sie eine neue Session.
SESSION_ENDEDEin 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))
TimeoutStandardMaximumGilt für
tapWait30 s60 sWarten auf das Vorhalten in openSession()
operation1 s5 sJeden Befehl auf dem Tag
reselect1 s5 sErneute Auswahl des Tags nach einem fehlgeschlagenen Befehl
idle10 s60 sZeit 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 mit READER_BUSY fehl. Zahlungen, die mit Accept.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 writeBlock kann 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:

  1. Prüfen Sie, dass isSupported() auf beiden true und auf einem Gerät ohne NFC false zurückgibt.
  2. 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 dann writePage aus und lesen Sie die Seite erneut.
  3. Authentifizieren Sie sich bei MIFARE Classic mit einem falschen Schlüssel und bestätigen Sie AUTH_FAILED, dann mit dem richtigen.
  4. Brechen Sie openSession() während des Wartens auf das Vorhalten ab und öffnen Sie sofort eine neue Session.
  5. Starten Sie während einer Session eine Zahlung: Die Session sollte als PAYMENT enden und die Zahlung durchgehen. Entfernen Sie während einer anderen Session den Tag: Sie sollte als CARD_REMOVED enden.
  6. 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 als ACTIVITY_PAUSED endet.
Fehlt Ihnen etwas auf dieser Seite?Wünschen Sie sich eine Funktion. Wir hängen diese Seite an, damit das Team den Kontext sieht.