Tag NFC
AndroidBetaĐọc và ghi tag NFC từ ứng dụng Android của bạn, bằng NFC của điện thoại hoặc đầu đọc của máy thanh toán Sunmi dòng P.
Đọc và ghi tag NFC, chẳng hạn thẻ khách hàng thân thiết, thẻ ra vào, nhãn dán và thẻ thông minh, trực tiếp từ ứng dụng Android của bạn. SDK giao tiếp với chính đầu đọc của thiết bị: Android NFC trên điện thoại, hoặc đầu đọc không tiếp xúc của máy thanh toán Sunmi dòng P. Không có plugin nào đứng giữa.
Phiên đọc tag được cung cấp trong các artifact tùy chọn, com.tapaya:accept-nfc và, cho máy Sunmi,
com.tapaya:accept-nfc-sunmi. Chỉ thêm các artifact này nếu bạn cần; thanh toán bằng thẻ vẫn hoạt động như nhau dù có hay không có chúng.
Thiết bị được hỗ trợ
| Thiết bị | Đầu đọc | Artifact bạn cần |
|---|---|---|
| Điện thoại hoặc máy tính bảng Android có NFC | Android NFC | accept-nfc |
| Máy thanh toán Sunmi dòng P | Đầu đọc không tiếp xúc của máy, thông qua dịch vụ thanh toán (pay service) của Sunmi | accept-nfc và accept-nfc-sunmi |
| Mọi thiết bị khác | Không có | Không được hỗ trợ |
Máy thanh toán Sunmi dòng P không có Android NFC; ăng-ten của chúng nằm phía sau mô-đun thanh toán. accept-nfc-sunmi
truy cập ăng-ten này thông qua dịch vụ thanh toán của Sunmi, và được dùng thay cho Android NFC bất cứ khi nào dịch vụ đó được cài đặt.
Trên thiết bị không có đầu đọc dùng được, isSupported() trả về false và openSession() thất bại với
NOT_SUPPORTED.
Điện thoại có thêm một vài giới hạn
Điện thoại chỉ đọc tag cho activity đang ở foreground, nên phiên sẽ kết thúc khi activity đó rời khỏi foreground.
MIFARE Classic chỉ hoạt động trên điện thoại có bộ điều khiển NFC của NXP; trên các điện thoại khác, các thao tác của nó thất bại với
NOT_SUPPORTED. Với tag ISO-DEP, atsHex chỉ chứa các byte lịch sử (historical bytes) của ATS, vì Android không cung cấp
phần còn lại.
Tag được hỗ trợ
| Loại tag | Ví dụ |
|---|---|
| NFC Type 2 | NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C |
| MIFARE Classic | MIFARE Classic Mini, 1K, 4K |
| Thẻ thông minh ISO-DEP | MIFARE DESFire |
Cách hoạt động
- Ứng dụng của bạn gọi
Accept.nfc.openSession(activity). - SDK bật đầu đọc của thiết bị và chờ chạm tag.
- Khách hàng chạm một tag, và
openSession()trả về mộtTagSessionmang UID và họ (family) của tag. - Ứng dụng của bạn đọc và ghi thông qua phiên, mỗi lần một lệnh.
- Ứng dụng của bạn gọi
close(), thao tác này giải phóng đầu đọc cho thanh toán và các phiên đọc tag khác.
Thanh toán luôn được ưu tiên
Mỗi thiết bị chỉ có một đầu đọc không tiếp xúc, và phiên đọc tag với thanh toán bằng thẻ không thể dùng chung đầu đọc đó. Việc bắt đầu
một khoản thanh toán bằng Accept.payments.pay() sẽ kết thúc phiên của bạn, và openSession() thất bại với READER_BUSY cho tới khi
khoản thanh toán kết thúc. Hãy sẵn sàng cho việc phiên có thể kết thúc bất cứ lúc nào; xem Kết thúc phiên.
Điều kiện tiên quyết
- Một thiết bị được hỗ trợ.
- Accept SDK đã được cài đặt và khởi tạo. Gọi
isSupported()hoặcopenSession()trướcAccept.initializesẽ ném raNotInitialized.
Phiên đọc tag không cần merchant đã đăng nhập, plugin Tapaya Terminal hay quyền truy cập vị trí. Các artifact tự thêm những gì chúng cần vào manifest của bạn:
accept-nfcthêm quyềnNFCvà đánh dấuandroid.hardware.nfclà không bắt buộc, để Play vẫn cung cấp ứng dụng của bạn trên máy Sunmi, vốn không có Android NFC.accept-nfc-sunmithêm quyềncom.sunmi.perm.CONTACTLESS_CARDvà một mục<queries>cho dịch vụ thanh toán của Sunmi. Artifact này nhúng sẵn Sunmi PayLib.
Tích hợp
Cài đặt
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")
}Nếu bạn cũng khai báo com.tapaya:accept, hãy giữ tất cả các artifact ở cùng một phiên bản.
Accept.nfc là một extension property; hãy import trước khi sử dụng:
import com.tapaya.accept.nfc.nfcKiểm tra khả năng hỗ trợ
Gọi isSupported() để quyết định có hiển thị tính năng tag hay không:
if (Accept.nfc.isSupported()) {
showScanTagButton()
}isSupported() không cần chạm và không động tới đầu đọc, nên có thể gọi an toàn bất cứ lúc nào. Hàm cho bạn biết thiết bị
có đầu đọc mà SDK dùng được. Hàm trả về true trên điện thoại đã tắt NFC; khi đó openSession() thất bại
với NFC_DISABLED. Hàm ném ra NotInitialized nếu Accept.initialize chưa chạy.
Mở phiên
Truyền vào activity đang ở foreground và ở trạng thái 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() tạm dừng (suspend) cho tới khi một tag được chạm hoặc hết tapWait (mặc định 30 giây). Hãy hiển thị lời nhắc “Chạm
thẻ của bạn” trong lúc chờ. Hàm ném ra IllegalStateException nếu activity không ở trạng thái resumed.
Trên điện thoại, activity đóng vai trò quan trọng: Android chỉ đọc tag cho activity đó, và khi activity rời khỏi foreground, việc chờ
sẽ dừng hoặc phiên kết thúc với ACTIVITY_PAUSED. Máy Sunmi đọc tag bất kể thứ gì đang ở foreground, nhưng bạn vẫn truyền vào một
activity.
Để dừng chờ, chẳng hạn khi khách hàng nhấn Cancel, hãy hủy coroutine đã gọi openSession().
Đầu đọc sẽ ngừng chờ, và bạn có thể mở một phiên mới ngay lập tức.
Đọc thông tin phiên
session.info mô tả tag đã được chạm:
| Trường | Mô tả |
|---|---|
uidHex | UID của tag dưới dạng hex viết hoa, ví dụ 04A1B2C3D4E5F6. |
family | TagFamily, cho bạn biết những thao tác nào áp dụng được. |
sak, atqaHex | Thông tin nhận dạng cấp thấp được trả về khi phát hiện tag. |
atsHex | ATS cho tag ISO_DEP; nếu không thì là null. |
memoryBytes | Dung lượng bộ nhớ người dùng, khi đầu đọc xác định được; nếu không thì là null. |
versionHex | Phản hồi GET_VERSION, với các tag hỗ trợ lệnh này; nếu không thì là null. |
Đọc và ghi tag
Rẽ nhánh theo session.info.family, sau đó dùng các thao tác dành cho loại 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.")
}Một lệnh không phù hợp với tag đã chạm sẽ thất bại với NOT_SUPPORTED, và phiên vẫn mở.
NFC Type 2
NTAG, MIFARE Ultralight, Ultralight EV1, Ultralight C và các tag Type 2 khác (TYPE2_OTHER).
| Thao tác | Chức năng |
|---|---|
readPages(startPage) | Trả về 16 byte: bốn trang 4 byte bắt đầu từ startPage. |
writePage(page, data) | Ghi một trang 4 byte. |
passwordAuth(password) | Chỉ dành cho NTAG và Ultralight EV1. Mở khóa các trang được bảo vệ bằng mật khẩu 4 byte và trả về PACK 2 byte, bạn có thể dùng PACK để kiểm tra tag có phải là tag chính hãng hay không. |
ultralightCAuth(key) | Chỉ dành cho Ultralight C. Xác thực bằng khóa 3DES 16 byte. |
transceiveType2(frame) | Gửi một frame Type 2 thô, cho các lệnh không có phương thức riêng, và trả về phản hồi của 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 và 4K. Hãy xác thực một sector trước khi đọc hoặc ghi các block của nó.
| Thao tác | Chức năng |
|---|---|
authenticateClassic(block, keyType, key) | Xác thực sector chứa block bằng một khóa 6 byte. |
readBlock(block) | Trả về 16 byte của block. |
writeBlock(block, data) | Ghi 16 byte vào block. |
increment, decrement, restore | Đưa kết quả của thao tác trên value block vào bộ đệm truyền (transfer buffer) của tag. |
transfer(block) | Ghi bộ đệm truyền vào một 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)Khóa sai sẽ thất bại với AUTH_FAILED, và SDK tự chọn lại (re-select) tag cho bạn, nên bạn có thể thử khóa tiếp theo
ngay lập tức:
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")Thẻ thông minh ISO-DEP
MIFARE DESFire và các thẻ ISO 14443-4 khác.
| Thao tác | Chức năng |
|---|---|
transmitApdu(apdu) | Gửi một APDU lệnh và trả về phản hồi: dữ liệu theo sau là 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 successKết thúc phiên
Gọi close() ngay khi bạn xong việc, tốt nhất là từ một khối finally. Cho tới khi bạn làm vậy, không khoản thanh toán nào có thể dùng
đầu đọc. Có thể gọi close() an toàn nhiều lần, và hàm không bao giờ ném ngoại lệ với một phiên đã kết thúc.
Phiên cũng có thể tự kết thúc. session.ended là một StateFlow giữ giá trị null trong khi phiên còn hoạt động,
sau đó phát ra lý do phiên kết thúc:
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 | Khi nào xảy ra |
|---|---|
CLOSED | Ứng dụng của bạn đã gọi close(). |
CARD_REMOVED | Tag đã rời khỏi vùng đọc và không quay lại trong thời gian chờ reselect. |
IDLE_TIMEOUT | Không có lệnh nào đến trong thời gian chờ idle (mặc định 10 giây). |
PAYMENT | Một khoản thanh toán đã bắt đầu qua Accept.payments.pay(). Thanh toán luôn được ưu tiên. |
ACTIVITY_PAUSED | Chỉ trên điện thoại. Activity mà bạn dùng để mở phiên đã rời khỏi foreground, và Android ngừng đọc tag cho activity đó. |
READER_LOST | Chỉ trên máy Sunmi. Kết nối tới dịch vụ thanh toán của Sunmi bị ngắt. |
Sau khi phiên đã kết thúc, mọi lệnh tiếp theo đều thất bại với SESSION_ENDED. Hãy gọi lại openSession() cho một lần
chạm mới.
Xử lý lỗi
Lỗi liên quan tới tag sẽ ném ra TagSessionException, với reason cho bạn biết cần làm gì tiếp theo. Gọi API trước khi
SDK được thiết lập sẽ ném ra NotInitialized. TagSessionException không phải là một AcceptException, vì vậy hãy bắt riêng
hai loại này:
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
}Xác thực lại khi authLost được đặt
Với bất kỳ lý do nào, cờ authLost của ngoại lệ đều có thể được đặt. Khi cờ này được đặt, hãy xác thực lại trước khi gửi
lệnh tiếp theo tới vùng nhớ được bảo vệ; với DESFire, hãy chọn lại cả ứng dụng (application).
Đối số khóa, block hoặc trang có độ dài hoặc dấu không đúng, hoặc frame hay APDU rỗng, sẽ ném ra
IllegalArgumentException trước khi bất cứ thứ gì đến được tag.
Mở phiên
reason | Khi nào xảy ra | Cần làm gì |
|---|---|---|
TIMEOUT | Không có lần chạm nào trong tapWait. | Nhắc lại, hoặc dừng. |
READER_BUSY | Một khoản thanh toán hoặc phiên khác đang dùng đầu đọc. | Thử lại sau; không có hàng đợi. |
SESSION_ENDED | Một khoản thanh toán đã bắt đầu, hoặc activity đã rời khỏi foreground, trong lúc chờ chạm. | Mở một phiên mới khi khoản thanh toán đã kết thúc hoặc ứng dụng đã quay lại foreground. |
NFC_DISABLED | Chỉ trên điện thoại. NFC bị tắt trong cài đặt hệ thống. | Yêu cầu người dùng bật NFC. |
VENDOR_ERROR | Máy Sunmi: dịch vụ thanh toán không khả dụng. vendorCode chứa mã lỗi của dịch vụ, nếu có. | Thử lại. Nếu lỗi vẫn tiếp diễn, hãy báo cho bộ phận hỗ trợ nhà phát triển kèm theo vendorCode. |
NOT_SUPPORTED | Thiết bị không có đầu đọc mà SDK dùng được. | Ẩn tính năng khi isSupported() trả về false. |
Gửi lệnh
reason | Khi nào xảy ra | Cần làm gì |
|---|---|---|
TIMEOUT | Tag không phản hồi kịp thời; tag đã được chọn lại. | Thử lại. Nếu authLost được đặt, hãy xác thực trước. |
NO_RESPONSE | Tag không phản hồi. | Thử lại nếu tag vẫn còn trong vùng đọc. |
AUTH_FAILED | Sai khóa hoặc mật khẩu. | Thử khóa tiếp theo; tag đã được chọn lại. |
NAK | Tag từ chối lệnh: trang nằm ngoài phạm vi, vùng nhớ được bảo vệ hoặc lệnh không được hỗ trợ. | Sửa lệnh. Phiên vẫn tiếp tục. |
NOT_SUPPORTED | Lệnh không phù hợp với họ tag này, hoặc với bộ điều khiển NFC của điện thoại này (MIFARE Classic cần NXP). | Kiểm tra session.info.family. Phiên vẫn tiếp tục. |
VENDOR_ERROR | SDK riêng của đầu đọc báo lỗi; vendorCode chứa mã lỗi. | Thử lại, sau đó báo cho bộ phận hỗ trợ nhà phát triển kèm theo vendorCode. |
Phiên đã kết thúc
reason | Khi nào xảy ra | Cần làm gì |
|---|---|---|
CARD_REMOVED | Tag đã rời khỏi vùng đọc trong lúc thực hiện lệnh. | Yêu cầu chạm lại và mở một phiên mới. |
SESSION_ENDED | Một lệnh được gửi sau khi phiên đã kết thúc. | Mở một phiên mới. |
Thời gian chờ
Truyền TagTimeouts vào openSession() để thay đổi giá trị mặc định:
Accept.nfc.openSession(activity, TagTimeouts(tapWait = 15.seconds, idle = 30.seconds))| Thời gian chờ | Mặc định | Tối đa | Áp dụng cho |
|---|---|---|---|
tapWait | 30 s | 60 s | Chờ chạm trong openSession() |
operation | 1 s | 5 s | Mỗi lệnh trên tag |
reselect | 1 s | 5 s | Chọn lại tag sau một lệnh thất bại |
idle | 10 s | 60 s | Khoảng thời gian giữa các lệnh trước khi phiên kết thúc |
Mọi giá trị đều phải lớn hơn 0. Tạo TagTimeouts với giá trị nằm ngoài các khoảng này sẽ ném ra
IllegalArgumentException.
Cần lưu ý
- Một đầu đọc. Thiết bị chỉ có một đầu đọc không tiếp xúc, nên mỗi lúc chỉ có một phiên đọc tag hoặc một lượt chờ chạm. Lệnh gọi
openSession()thứ hai sẽ thất bại vớiREADER_BUSY. Khoản thanh toán bắt đầu bằngAccept.payments.pay()sẽ kết thúc phiên trước; khoản thanh toán bắt đầu bên ngoài SDK thì không thể điều phối, vì vậy hãy giữ phiên ngắn gọn. - Hủy một lệnh không hoàn tác lệnh đó. Vào lúc bạn hủy coroutine, lệnh đã tới tag
và vẫn được thực thi; chỉ kết quả của nó bị bỏ đi. Một
writeBlockđã bị hủy vẫn có thể thay đổi tag. - Các lệnh chạy theo thứ tự. Có thể gọi các phương thức của phiên từ bất kỳ coroutine nào; chúng tới tag lần lượt từng lệnh một, theo đúng thứ tự bạn gọi.
- Dữ liệu tag không xuất hiện trong log. SDK không bao giờ ghi log khóa, mật khẩu hay dữ liệu tag, và che UID (ví dụ
04A1…E5F6). Việc ứng dụng của bạn lưu trữ và hiển thị dữ liệu tag như thế nào là do bạn quyết định.
Kiểm thử tích hợp
Trên điện thoại và, nếu bạn hỗ trợ, trên máy thanh toán Sunmi dòng P:
- Kiểm tra rằng
isSupported()trả vềtruetrên cả hai, vàfalsetrên thiết bị không có NFC. - Chạm một tag NTAG hoặc Ultralight và xác minh rằng phiên báo đúng UID và
readPages(0)trả về 16 byte. Sau đó thực hiệnwritePagevà đọc lại trang đó. - Trên MIFARE Classic, xác thực bằng một khóa sai và xác nhận nhận được
AUTH_FAILED, sau đó xác thực bằng khóa đúng. - Hủy
openSession()trong lúc chờ chạm và mở ngay một phiên mới. - Bắt đầu một khoản thanh toán trong khi phiên đang mở: phiên sẽ kết thúc với
PAYMENTvà khoản thanh toán được thực hiện thành công. Lấy tag ra trong một phiên khác: phiên sẽ kết thúc vớiCARD_REMOVED. - Trên điện thoại, tắt NFC và xác nhận nhận được
NFC_DISABLED. Trong khi phiên đang mở, đưa ứng dụng xuống chạy nền và xác nhận phiên kết thúc vớiACTIVITY_PAUSED.