Skip to main content

Android adapters & demo app

The repository ships a debug wallet under demo/ (Jetpack Compose) plus a set of reusable Android platform-adapter libraries under android/ that implement the SDK's ports. Your app depends on the android/ libraries instead of copying demo code.

The android/ adapter libraries​

Four modules under the Maven group com.hopae.eudi.android (distinct from the SDK's com.hopae.eudi, so artifacts never clash):

ModuleProvidesKey classes
coreTier-1 portsAndroidKeystoreSecureArea (SecureArea, hardware-backed), FileStorageDriver (StorageDriver), OkHttpTransport (HttpTransport), FileTransactionLogStore (TransactionLogStore)
proximityISO 18013-5 transportsBleGattClientTransport / BleGattServerTransport (ProximityTransport), NfcEngagementService (holder HCE), NfcReader (reader). Its library manifest merges the BLE/NFC permissions + the HCE service into your app
dcapiDigital Credentials APIDcApiRegistrar (Credential Manager registration + matcher), DcApiRequest / DcApiResult (envelope + marshalling), DcApiBranding (OS-selector logo/branding)
attestationWallet Provider linkWalletProviderAttestation (WalletAttestationProvider, talks to a wallet-provider/ backend for WUA + key attestation), PlayIntegrityTokenProvider (device integrity, with a DevIntegrityTokenProvider fallback for side-loaded debug builds)

Assembling a Wallet​

Everything is host-injected through WalletPorts — no DI framework:

val logger = LogWalletLogger() // your WalletLogger (the demo routes to logcat + on-screen + file)
val wallet = Wallet.create(
config = WalletConfig(),
ports = WalletPorts(
secureAreas = listOf(AndroidKeystoreSecureArea()), // android/core — hardware keys
storage = FileStorageDriver(File(filesDir, "wallet")), // android/core
http = OkHttpTransport(logger = logger), // android/core
transactionLogStore = FileTransactionLogStore(File(filesDir, "tx.log")), // android/core
logger = logger,
// walletAttestation = WalletProviderAttestation(...) // android/attestation — WUA + key attestation
),
)

Port coverage. The android/ libraries cover every required port (SecureArea, StorageDriver, HttpTransport) plus TransactionLogStore, the proximity transports, and WalletAttestationProvider. WalletClock / Rng use the SDK defaults (WalletClock.System / Rng.Default). WalletLogger is intentionally app-supplied — real logging (screen/file) is app-specific, so no concrete logger ships in android/core.

Wallet Provider attestation​

android/attestation implements WalletAttestationProvider against a wallet-provider/ backend: a WUA for attestation-based client authentication and per-issuance key attestation, with the device attested by Play Integrity (falling back to a dev token on side-loaded builds). Wire it only if your deployment needs it — issuance against issuers that accept a public client_id works without it.

val walletAttestation = WalletProviderAttestation(
baseUrl = "https://your-wallet-provider.example/wp",
http = http,
secureArea = secureArea, // signs the instance-key proof of possession
integrity = PlayIntegrityTokenProvider(context, gcpProjectNumber, fallback = DevIntegrityTokenProvider(), logger = logger),
clientId = "wallet-dev", // must equal IssuanceConfig.clientId so the WUA `sub` matches at the issuer
storage = storage, // persists the instance registration id across restarts
)
// → WalletPorts(..., walletAttestation = walletAttestation)

Proximity (BLE + NFC)​

Build a transport and hand it to the facade. Both BLE modes and NFC static + negotiated handover are implemented and phone-to-phone device-verified (see INTEROP.md).

// Holder over BLE peripheral-server:
val server = BleGattServerTransport(context, uuid, Ble.PERIPHERAL_SERVER, retrievalMethods, logger = logger)
val session = wallet.proximity.present(server) // QR engagement

// Reader:
val docs = wallet.reader.read(clientTransport, engagement, requested)

For NFC the holder arms the HCE with an NfcEngagementProcessor (static, or a negotiated hr → hs selector); the reader drives NfcReader.readHandover(...) which auto-detects static vs the TNEP negotiated dance. In the negotiated case the holder picks its BLE role out of what the reader offered (NfcHandoverRequest.selectCarrier()) instead of imposing one — see who picks the BLE mode.

Both roles take an optional tap callback — readHandover(activity) { … } and NfcEngagementService.onEngaged — which fire the instant the tag is taken, before any APDU. The demo vibrates from them: a tap is over in milliseconds while the handover plus the BLE connection run for seconds after it, so without that confirmation people pull the phones apart mid-exchange.

The demo defaults to NFC negotiated handover and, for QR, the Central Bluetooth role — both landing on mdoc central client mode, where the reader advertises and the wallet only scans. Settings switches either. See demo/.../ui/ProximityScreens.kt for the full lifecycle, the Proximity guide, and demo/PROXIMITY-MATRIX.md for the device test matrix you should re-run after touching any of it.

Digital Credentials API​

DcApiRegistrar.register(activity, wallet, DcApiBranding(logoPng = appIconPng()), logger = logger)

Registers the wallet's credentials with the Credential Manager (the OS credential selector), branded with your app's icon. Handle the routed intent in a thin activity with DcApiRequest / DcApiResult (see the demo's GetCredentialActivity).

Build​

The demo consumes the SDK and the adapters via composite builds (includeBuild("../kotlin") + includeBuild("../android")):

// demo/app/build.gradle.kts
implementation("com.hopae.eudi:wallet:0.0.1-SNAPSHOT")
implementation("com.hopae.eudi.android:core:0.0.1-SNAPSHOT")
implementation("com.hopae.eudi.android:proximity:0.0.1-SNAPSHOT")
implementation("com.hopae.eudi.android:dcapi:0.0.1-SNAPSHOT")
cd demo
./gradlew :app:assembleDebug # → app/build/outputs/apk/debug/app-debug.apk
adb install -r app/build/outputs/apk/debug/app-debug.apk

Toolchain: AGP 9.2.1, Gradle 9.5, compileSdk 36, minSdk 29. The android/ build needs android/local.properties with sdk.dir=/path/to/android-sdk.