4.1 KiB
Tap to Pay
BML contactless payment via Android Host Card Emulation. The app emulates a BML magnetic-stripe contactless card using a single-use wallet token fetched from the BML Tap-to-Pay API.
Entry Points
| Trigger | Path |
|---|---|
Manifest NFC service launches BmlTapToPayActivity |
Redirects to MainActivity with TAP_TO_PAY |
TAP_TO_PAY intent |
R.id.nav_pay_with_card + auto_tap_mode=true |
| "Tap to Pay" button on the Cards screen | CardsFragment.setTapMode(true, item) |
| Dashboard NFC button on a BML card | Same |
BmlTapToPayActivity (nfc/BmlTapToPayActivity.kt) is a 7-line trampoline — it sets the TAP_TO_PAY action and FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK and finishes.
Pre-flight — NfcPaymentUtil
Located at util/NfcPaymentUtil.kt. Every tap-to-pay entry runs checkAndProceed(context, onReady) first:
NfcAdapter.getDefaultAdapter()— null → "NFC unsupported" dialognfcAdapter.isEnabled— false → "Turn on NFC" dialog with a button that opensSettings.ACTION_NFC_SETTINGSCardEmulation.isDefaultServiceForCategory(component, CATEGORY_PAYMENT)— false → "Set this app as the default payment app" dialog with a button that opensSettings.ACTION_NFC_PAYMENT_SETTINGS- All three pass →
onReady()is invoked
If the user has biometrics_transfer_confirm enabled, CardsFragment.showBiometricPromptForTap() runs a BIOMETRIC_STRONG prompt before entering tap mode.
Token Fetch
fetchAndArmToken() (in CardsFragment):
- Locate the BML session and OTP seed for the selected card's login
- Generate a fresh TOTP via
util/Totp BmlTapToPayClient().fetchTokens(session, internalId, otp)returns a list ofBmlWalletToken; the first is usedBmlHostCardEmulatorService.setToken(token)arms the HCE serviceBmlHostCardEmulatorService.onTransactionComplete = { success -> ... }— re-enters the carousel and shows a success toast / refresh on success
If any step fails, a toast is shown and tap mode exits.
HCE Service — BmlHostCardEmulatorService
Subclass of HostApduService. Implements the minimal EMV mag-stripe contactless flow:
SELECT PPSE → SELECT AID → GET PROCESSING OPTIONS → READ RECORD
APDU Handling
| INS | Handler | Response |
|---|---|---|
A4 SELECT (data = PPSE name 2PAY.SYS.DDF01) |
handleSelect |
FCI containing app entry: tag 4F = AID, tag 87 = priority 01, DF Name 84 = PPSE |
A4 SELECT (data = AID bytes from token) |
handleSelect |
FCI containing AID, ASCII label (VISA / MASTERCARD / AMEX / BML), PDOL 9F38 = 9F6602 (TTQ 2 bytes) |
A8 GPO |
handleGpo |
AIP 0080 (mag-stripe mode), AFL 08010100 (SFI=1, record 1-1) |
B2 READ RECORD |
handleReadRecord |
Tag 70 containing tag 57 Track 2 — fires onTransactionComplete(true) |
| Anything else | — | 6D00 (INS not supported) |
PPSE selection when no active token is set fires launchPromptActivity() which starts BmlTapToPayActivity to bring the user into the app.
Track 2
buildTrack2(token): "${token.token}D${token.expiry}${token.serviceCode}${token.data}", padded with F to even length.
Companion State
| Member | Use |
|---|---|
activeToken: BmlWalletToken? |
The armed token (volatile) |
onTransactionComplete: ((Boolean) -> Unit)? |
UI callback invoked from READ RECORD or onDeactivated |
setToken(token) / clearToken() |
Called from CardsFragment.fetchAndArmToken() / exitTapMode() |
applicationLabel(aidHex) |
Maps Visa/MC/Amex AIDs to ASCII labels |
onDeactivated() fires onTransactionComplete(false) if the reader dropped before GPO was sent.
Failure Modes
- No NFC, NFC off, or app not the default payment service — handled by
NfcPaymentUtilwith dialogs that deep-link to Settings - Missing OTP seed or session — toast, tap mode exits
- Token fetch fails — toast, tap mode exits
- Reader removed before READ RECORD —
onDeactivatedreportssuccess = false; no toast is shown
← Cards Next → Notifications