Android setup
Add the Folio SDK to an Android app, initialize the platform, create the runtime, mount stores and run an inquiry with the Compose inquiry UI.
Preview
This page describes a pre-release version of the SDK. Names, versions and APIs on this page can change before the release.
This page adds the Folio SDK to an Android app. It covers the Gradle setup, the manifest, platform initialization, the runtime, stores and the inquiry UI. For the concepts behind the runtime and the stores, read Runtime first.
Requirements
| Item | Value |
|---|---|
minSdk | 23 or higher |
compileSdk | The SDK is compiled against API level 34 |
| Java | Source and target compatibility 17, Kotlin jvmTarget 17 |
| Android Gradle Plugin | The SDK is built with 8.13.0 |
| Kotlin | The SDK is built with 2.2.10 |
| UI toolkit | Jetpack Compose (the inquiry UI and rememberFolioSdk) |
| ABIs | arm64-v8a, armeabi-v7a, x86_64 |
Artifacts
| Artifact | Contents |
|---|---|
id.folio:sdk | The Kotlin API in id.folio.sdk: FolioSdk, rememberFolioSdk, FolioSDKProvider, the stores (SessionStore, AuthStore, VaultStore, InquiryStore, MrtdStore, LogsStore, FolioDocumentListStore, FolioDocumentStore) with their actions, state types and remember composables. The inquiry UI in id.folio.sdk.inquiry. The platform layer XPlatform in id.folio.sdk.platform. The icon images of FolioSdk.iconAsset and their resolver FolioResourceIcons in id.folio.sdk.resources; see Static functions. DefaultSelfieCaptureProvider in id.folio.sdk.inquiry.ui.verification.selfie, the selfie capture provider of the inquiry UI, built on the capture engine of the selfie step |
id.folio:sdk-native | The native library of the SDK for the ABIs above |
An app that runs the SDK adds id.folio:sdk only. Its POM depends on id.folio:sdk-native of the
same release and on the capture engine of the selfie step, so Gradle pulls both in (see
Dependencies).
id.folio:sdk-native holds two shared libraries per ABI: libid_folio.so with the SDK core and
libid_folio_jni.so with the JNI bridge, which is linked for 16 KB memory pages. Its sources jar
carries the C sources of the JNI bridge. id.folio:sdk
depends on id.folio:sdk-native of its own version; do not force another version of it. When the
Kotlin API first loads the native libraries, it compares their bindgen format version and metadata
digest with its own. When they differ, the first SDK call throws an ExceptionInInitializerError
whose cause is an IllegalStateException that names both values, and every later call throws
NoClassDefFoundError.
The sha256 of libid_folio.so in the provenance file and in
FolioSDKBuildInfo.NATIVE is the digest of the file in the Maven repository,
jni/<abi>/libid_folio.so inside the id.folio:sdk-native AAR. When the Android Gradle plugin
packages your APK, it strips the library and rewrites its ELF section tables, so the copy inside the
APK has another digest, in debug and release builds alike. To keep the published bytes in your APK,
exclude the library from stripping; otherwise compare the AAR, not the APK:
android {
packaging {
jniLibs {
keepDebugSymbols += "**/libid_folio.so"
}
}
}Install
Repositories
Folio gives you access to its Maven repository. Add it to your Gradle settings next to Google and Maven Central. The Folio repository also serves the capture engine of the selfie step, so no other repository is needed:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("<android-maven-repository-url>") }
}
}Dependencies
dependencies {
implementation("id.folio:sdk:<version>")
}<version> is the SDK release version Folio gives you.
id.folio:sdk pulls in id.folio:sdk-native of the same version and the capture engine of the
selfie step at fixed versions, which the Folio Maven repository serves.
id.folio:sdk exposes as API dependencies the libraries its public types use, so your app compiles
against them without declaring them: androidx.activity:activity-compose 1.9.3,
androidx.lifecycle:lifecycle-viewmodel 2.7.0, androidx.compose.runtime:runtime,
androidx.compose.foundation:foundation, androidx.compose.ui:ui and androidx.compose.ui:ui-text
1.7.5, org.jetbrains.kotlinx:kotlinx-coroutines-android 1.7.3 and
org.jetbrains.kotlinx:kotlinx-serialization-json 1.7.3. These are minimum versions: Gradle
resolves each of them to the newer version your app declares. The identity record mapper you
write (see Create the runtime) encodes your own JSON document; the samples
on this page use kotlinx.serialization for it, which needs the Kotlin serialization plugin in your
app module.
Compose and Kotlin plugins
rememberFolioSdk, FolioSDKProvider and FolioInquiry.Host are composables. Enable Compose with
the Kotlin Compose compiler plugin and set Java 17:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.serialization")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
compileSdk = 34
defaultConfig {
minSdk = 23
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
compose = true
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}JvmTarget is org.jetbrains.kotlin.gradle.dsl.JvmTarget.
A release published again
Folio can publish a release again under the same version. Gradle caches the artifacts of a version
it has downloaded and keeps using them. After a release is published again, run your build once
with --refresh-dependencies, so that Gradle checks the cached artifacts against the repository
and downloads the ones that changed:
./gradlew assembleDebug --refresh-dependenciesWhen <android-maven-repository-url> is a file URL of a local clone of the Maven repository,
Gradle reads the artifacts from the clone and caches nothing from it, so --refresh-dependencies
changes nothing. Update the clone instead, then build again:
git fetch --tags --force
git checkout v<version>To confirm the build your app runs, read the revision of the Stamped product of
FolioSdk.sdkBuildInfo(), or compare FolioSDKBuildInfo.NATIVE
with the provenance file provenance/<version>.json of the
release.
Manifest
What the SDK merges
The manifest of id.folio:sdk merges into your app's manifest. It declares:
| Entry | Notes |
|---|---|
android.permission.INTERNET | |
android.permission.ACCESS_NETWORK_STATE | |
android.permission.POST_NOTIFICATIONS | |
android.permission.NFC | Chip reading of passports and ID cards |
android.permission.CAMERA | Document and selfie capture in the inquiry UI |
uses-feature android.hardware.nfc | required="false" |
uses-feature android.hardware.camera | required="false" |
uses-feature android.hardware.camera.any | required="false" |
receiver id.folio.sdk.platform.XPlatformNotificationReceiver | exported="false" |
id.folio:sdk-native merges no entries.
The inquiry UI asks the user for the camera permission at runtime when it opens the camera. When the user denies it, the camera screen shows its denied state.
POST_NOTIFICATIONS and the receiver serve the notification service of the SDK's platform layer,
which products built on the SDK, such as Folio's own apps, use. None of the stores on this page
posts a notification, so with the SDK alone the receiver never fires and no launch intent carries
the SDK's folioTarget extra.
What the dependencies of the SDK merge
The libraries id.folio:sdk depends on merge their own manifest entries into your app. Among
others:
| Dependency | Entries |
|---|---|
androidx.work:work-runtime 2.9.0 | WAKE_LOCK, RECEIVE_BOOT_COMPLETED, FOREGROUND_SERVICE and ACCESS_NETWORK_STATE; the receiver androidx.work.impl.background.systemalarm.RescheduleReceiver on BOOT_COMPLETED, TIME_SET and TIMEZONE_CHANGED, and the other receivers, services and the provider of WorkManager; through Room, which WorkManager depends on, the service androidx.room.MultiInstanceInvalidationService |
androidx.biometric:biometric 1.1.0 | USE_BIOMETRIC and USE_FINGERPRINT |
| The capture engine of the selfie step | VIBRATE, CAMERA and FLASHLIGHT; uses-feature android.hardware.camera.autofocus with required="false"; the activities of the engine and a Bluetooth service of the engine; android:largeHeap="true" on the application |
androidx.camera:camera-camera2 1.3.4 | The disabled service androidx.camera.core.impl.MetadataHolderService with its default configuration provider |
androidx.credentials and Google Play services sign-in | The activities and the services of the credential provider and of Google sign-in, and the meta-data com.google.android.gms.version |
androidx.core | The permission <applicationId>.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION |
androidx.startup and androidx.profileinstaller | The provider androidx.startup.InitializationProvider with the initializers of WorkManager, of androidx.emoji2 and androidx.lifecycle:lifecycle-process (through Compose UI), of OkHttp and of androidx.profileinstaller, and the receiver androidx.profileinstaller.ProfileInstallReceiver |
RescheduleReceiver reschedules WorkManager work after a reboot and after a change of the time or
the time zone. It does not restore the SDK's notifications.
The SDK and its dependencies merge every permission the SDK uses, so your app adds none.
WAKE_LOCK, RECEIVE_BOOT_COMPLETED and FOREGROUND_SERVICE arrive through WorkManager,
USE_BIOMETRIC and USE_FINGERPRINT through androidx.biometric, and VIBRATE through the
capture engine of the selfie step.
The platform layer schedules a notification as an inexact alarm (setAndAllowWhileIdle), which
needs no exact-alarm permission; the system delivers it with a delay it chooses. The SDK registers
no receiver of its own for BOOT_COMPLETED, so a scheduled notification that is still pending at a
reboot is lost.
Initialize the platform
Call XPlatform.init once per process, before you create a runtime. The right place is the
onCreate of your Application subclass:
class ExampleApplication : Application() {
override fun onCreate() {
super.onCreate()
XPlatform.init(this)
}
}XPlatform is in the id.folio.sdk.platform package, imported as
import id.folio.sdk.platform.XPlatform. The rules:
inittakes anyContextand keeps its application context.initloads the SDK's native library and hands the application context to it. Never callSystem.loadLibraryfor the SDK yourself.- A second call throws
IllegalStateExceptionwith the message "XPlatform already initialized". An activity can be created more than once in a process, so do not callinitfrom an activity.XPlatform.isInitializedtells you whetherinithas run. initregisters activity lifecycle callbacks that track the resumed activity. NFC reading, biometric prompts and notifications use that activity.- Creating a runtime before
initfails.
Create the runtime
Configuration
The runtime starts from a FolioSdkConfig:
private val folioConfig = FolioSdkConfig(
baseUrl = "https://app.folio.mobi",
organization = "<organization>",
logging = FolioSdk.loggingConfig(null),
)| Field | Type | Value |
|---|---|---|
baseUrl | String | The Folio origin, https://app.folio.mobi |
organization | String | The organization Folio gives you. The SDK builds your vault record store for it |
development | DevelopmentConfig? | Defaults to null. A realm and service environments; see Configuration. |
logging | LoggingConfig | File and console writers for this runtime; see Logging. |
The SDK reads your app's identity from the package: the package name as the identifier,
versionName as the version and longVersionCode as the build (versionCode below API 28).
In a stamped build, create fails when versionName is missing or empty (see
Application identity). The SDK reports the version and
build in its user agent.
All types are in id.folio.sdk. See Configuration for every field in detail.
Identity record mapper
The runtime takes an IdentityRecordMapper next to the configuration and keeps it for its
lifetime. When a government ID verification succeeds, the inquiry calls
map(identity, existing) and writes the RecordContent you return to the vault. payload is your
own JSON document; schema is your name for it. existing is the record the SDK saved earlier for
the same physical document, or null for a new one. An exception thrown from map writes nothing
and ends the inquiry with InquiryError.RecordMapping. See
Identity records for the full contract.
@Serializable
data class SavedIdentity(
val identities: List<VerifiedIdentity>,
)
object SavedIdentityMapper : IdentityRecordMapper {
const val SCHEMA = "identity"
const val VERSION = 1L
override fun map(identity: VerifiedIdentity, existing: RecordContent?): RecordContent {
val earlier = existing?.let { record ->
require(record.schema == SCHEMA && record.version == VERSION) {
"a ${record.schema} v${record.version} record is no saved identity"
}
Json.decodeFromString<SavedIdentity>(record.payload).identities
}.orEmpty()
return RecordContent(
schema = SCHEMA,
version = VERSION,
payload = Json.encodeToString(SavedIdentity(identities = earlier + identity)),
)
}
}rememberFolioSdk
rememberFolioSdk(config, mapper) creates a FolioSdk and returns its startup state as a
FolioSdkStartup.
| State | Meaning |
|---|---|
FolioSdkStartup.Starting | The runtime is being created |
FolioSdkStartup.Failed | Creation failed; error is the Throwable that creation threw, retry() creates the runtime again |
FolioSdkStartup.Ready | The runtime is ready; runtime is the FolioSdk |
The runtime lives in a FolioSdkHolder, a ViewModel of the nearest ViewModelStoreOwner, such
as your activity:
- Creation runs in the view model's scope. A configuration change that recreates the activity keeps the same runtime.
- When the owner is destroyed for good, the holder cancels a creation in progress and closes the
runtime. Do not close a runtime from
rememberFolioSdkyourself. - A call with a different
config(compared withequals,FolioSdkConfigis a data class) or a differentmapperinstance (compared by identity) cancels the running creation, closes the current runtime and creates a new one; the call returnsStartinguntil it is ready. Keep the mapper one instance, such as a Kotlinobject, so that a recreated activity does not restart the runtime. retry()creates the runtime again with the sameconfigandmapper. It does nothing when theFailedvalue is no longer the current state.
setContent {
MaterialTheme {
when (val startup = rememberFolioSdk(folioConfig, SavedIdentityMapper)) {
FolioSdkStartup.Starting -> Unit
is FolioSdkStartup.Failed -> {
LaunchedEffect(startup) {
Log.e(TAG, "runtime creation failed", startup.error)
}
}
is FolioSdkStartup.Ready -> Verification(startup.runtime, launchCode)
}
}
}Without Compose
FolioSdk.create(config, mapper) is a suspend function that returns the FolioSdk or throws
when creation fails. Cancelling the calling coroutine cancels the creation. A runtime you create
this way is yours to close. close() and shutdown() are suspend functions. close() shuts the
runtime down once and releases it, even when the shutdown throws; it runs to the end when the
calling coroutine is cancelled. A second close() returns the result of the first one; any other
call on a closed runtime throws IllegalStateException. shutdown() unmounts every store and
shuts the session scope down without releasing the handle; close() calls it for you.
lifecycleScope.launch {
val runtime = FolioSdk.create(folioConfig, SavedIdentityMapper)
}Static functions
The runtime does not return data from its methods. Everything goes through stores. Functions that
need no runtime are @JvmStatic members of the companion object of FolioSdk, such as
FolioSdk.localize(key, locale), FolioSdk.availableLocales(), FolioSdk.countries() and
FolioSdk.sdkBuildInfo(). See Static functions for the full list
and Localization.
Stores
A store exists only while it is mounted on the runtime. Every Android store has the same shape:
| Member | Behaviour |
|---|---|
Store.mount(runtime) | Mounts the store and reads its initial state synchronously. Throws FolioSDKException on failure, IllegalStateException when the runtime is closed |
state | A StateFlow of the store's UI model. The value is never null |
dispatch(action, context) | Sends one action. context is an optional LogContext, null by default, that ties the action's journal entries to an interaction; see Logging. Throws FolioSDKException synchronously when the call fails |
streamError | A StateFlow<Throwable?>. It holds the error when the stream that keeps state current fails, and null otherwise |
subscribe() | Returns an AutoCloseable subscription, such as VaultSubscription. Its suspend next() returns the current UI model first, then the latest one after each change, and null once the store is unmounted; asFlow() exposes the same values as a Flow |
close() | Unmounts the store and cancels its running work. A second call does nothing. state keeps its last value; dispatch and subscribe after close() throw IllegalStateException |
mount takes the host interface of its store, such as VaultHost for VaultStore; FolioSdk
implements all of them.
| Store | State | Action | Host | Composable | Page |
|---|---|---|---|---|---|
SessionStore | SessionUiModel | SessionAction | SessionHost | rememberSessionStore() | Session |
AuthStore | AuthUiModel | AuthAction | AuthHost | rememberAuthStore() | Authentication |
VaultStore | VaultUiModel | VaultAction | VaultHost | rememberVaultStore() | Vault |
InquiryStore | InquiryUiModel | InquiryAction | InquiryHost | rememberInquiryStore() | Inquiry store |
MrtdStore | MrtdUiModel | MrtdAction | MrtdHost | rememberMrtdStore() | NFC |
LogsStore | LogsUiModel | LogsAction | DiagnosticsHost | rememberLogsStore(init) | Logging |
FolioDocumentListStore | FolioDocumentListUiModel | FolioDocumentListAction | FolioDocumentListHost | rememberFolioDocumentListStore() | Folio documents |
FolioDocumentStore | FolioDocumentUiModel | FolioDocumentAction | FolioDocumentHost | rememberFolioDocumentStore(init) | Folio documents |
FolioSDKException carries a numeric code and a message; its companion object names the codes,
such as FolioSDKException.INVALID_ARGUMENT.
The result of an action lands in the store's state, never in a return value. Closing a store cancels what it is doing, so keep a store mounted until the state shows the result you wait for.
In Compose, mount a store with remember, close it with DisposableEffect and collect its state
with collectAsState:
@Composable
fun SavedIdentities(runtime: FolioSdk) {
val vault = remember(runtime) { VaultStore.mount(runtime) }
DisposableEffect(vault) {
onDispose { vault.close() }
}
val model by vault.state.collectAsState()
val identities = model.records
.filter { it.header.schema == SavedIdentityMapper.SCHEMA }
.map { Json.decodeFromString<SavedIdentity>(it.payload) }
}FolioSDKProvider
FolioSDKProvider(runtime) { ... } puts a runtime into the composition. Inside it,
LocalFolioSdk.current is the FolioSdk, and every host local, such as LocalVaultHost, holds the
same runtime. Each remember<Name>Store() composable from the table above reads its host local,
mounts its store on that host with remember, and closes it with DisposableEffect when it leaves
the composition, as the sample above does by hand. Reading LocalFolioSdk or a host local, or
calling a store composable, outside a provider throws IllegalStateException.
setContent {
MaterialTheme {
when (val startup = rememberFolioSdk(folioConfig, SavedIdentityMapper)) {
FolioSdkStartup.Starting -> Unit
is FolioSdkStartup.Failed -> Button(onClick = startup::retry) { Text("Retry") }
is FolioSdkStartup.Ready -> FolioSDKProvider(startup.runtime) {
SavedIdentities()
}
}
}
}
@Composable
fun SavedIdentities() {
val vault = rememberVaultStore()
val model by vault.state.collectAsState()
val identities = model.records
.filter { it.header.schema == SavedIdentityMapper.SCHEMA }
.map { Json.decodeFromString<SavedIdentity>(it.payload) }
}FolioSDKProvider provides every host interface of the runtime. To provide one host only, use
SessionHostProvider, AuthHostProvider, VaultHostProvider, InquiryHostProvider,
MrtdHostProvider, DiagnosticsHostProvider, FolioDocumentListHostProvider or
FolioDocumentHostProvider, which take the host and content the same way and set
LocalSessionHost, LocalAuthHost, LocalVaultHost, LocalInquiryHost, LocalMrtdHost,
LocalDiagnosticsHost, LocalFolioDocumentListHost or LocalFolioDocumentHost. The matching
remember<Name>Store() works below such a provider.
rememberInquiryStore() mounts an InquiryStore without opening an inquiry; to run one, use
FolioInquiry.open (see Run an inquiry).
Run an inquiry
Your backend creates the inquiry and passes its launch code to your app. The inquiry UI is in
id.folio.sdk.inquiry.FolioInquiry. For the flow itself, see Inquiry UI.
Open the store
FolioInquiry.open(runtime, entry) takes an InquiryHost, such as your FolioSdk, mounts an
InquiryStore and dispatches InquiryAction.Open(entry). If the dispatch throws, open closes the
store and rethrows.
| Entry | Use |
|---|---|
InquiryEntry.Start(launchCode) | Start from the single-use launch code your backend received |
InquiryEntry.Resume(ResumeSource.SameDevice(inquiryId)) | Resume an inquiry on this device; inquiryId is nullable |
InquiryEntry.Resume(ResumeSource.AnotherDevice(code)) | Continue an inquiry handed off from another device |
Host the flow in Compose
FolioInquiry.Host(store, platform) renders the inquiry flow as a composable. platform is an
InquiryPlatform:
class InquiryPlatform(
val nfc: MrtdHost,
val selfieCapture: SelfieCaptureProvider = DefaultSelfieCaptureProvider(),
)nfc is the runtime that reads the chip, your FolioSdk. selfieCapture is the
selfie engine, DefaultSelfieCaptureProvider() unless you pass your own.
@Composable
private fun Verification(runtime: FolioSdk, launchCode: String) {
val store = remember(runtime, launchCode) {
FolioInquiry.open(runtime, InquiryEntry.Start(launchCode))
}
DisposableEffect(store) {
onDispose { store.close() }
}
val platform = remember(runtime) {
InquiryPlatform(nfc = runtime)
}
val model by store.state.collectAsState()
LaunchedEffect(model.lifecycle) {
when (val lifecycle = model.lifecycle) {
is InquiryLifecycle.Ready -> Log.i(TAG, "inquiry ready: sdk ${lifecycle.sdkVersion}")
is InquiryLifecycle.Started -> Log.i(TAG, "inquiry started: ${lifecycle.correlationId}")
is InquiryLifecycle.Closed -> {
Log.i(TAG, "inquiry closed: ${lifecycle.outcome}")
finish()
}
is InquiryLifecycle.Failed -> Log.e(TAG, "inquiry failed: ${lifecycle.error}")
}
}
FolioInquiry.Host(store, platform)
}Host does not close the store or finish your activity: react to InquiryLifecycle.Closed yourself
and close the store then. Closed arrives only after the flow dispatched InquiryAction.Close;
Failed is informational, the flow still shows the failure notice and the person leaves through
its close button.
System Back
Host and present handle the system Back gesture and button themselves; add no BackHandler of
your own around them. Back does what the screen allows:
| On screen | Back |
|---|---|
| An open sheet, such as an option list or a dialog | Closes the sheet. |
A step that can go back (navigation.canGoBack) | Goes back one step, as the back button of the step does. |
| A step with a top bar but no way back | Asks whether to leave the inquiry, as the bar's close button does; leaving dispatches Close. |
| The terminal stage, the loading state or a failure notice | Dispatches Close. |
After the flow dispatched Close it sends nothing more to the store until you dispatch the next
Open to the same store, and it handles no Back while lifecycle is Closed.
Present in an activity
FolioInquiry.present(activity, store, platform) takes a ComponentActivity and:
- enables edge-to-edge with transparent status and navigation bars;
- calls
setContenton that activity with the inquiry flow inside aMaterialThemeand a full-sizeSurface; - finishes the activity when
lifecyclebecomesInquiryLifecycle.Closed, unless it is already finishing.
FolioInquiry.present(activity = this, store = store, platform = platform)present replaces the content of the activity instance you pass. It does not start a new activity,
and it does not close the store.
Lifecycle and state
The inquiry UI dispatches InquiryAction.Close when the user leaves the flow: from the terminal
stage after the redirect, from a close button or the failure notice, or on Back as described above.
lifecycle becomes Closed only after that Close; while the terminal stage shows it stays
Started. Watch InquiryUiModel.lifecycle:
| Lifecycle | Field |
|---|---|
InquiryLifecycle.Ready | sdkVersion |
InquiryLifecycle.Started | correlationId |
InquiryLifecycle.Closed | outcome, a TerminalOutcome or null |
InquiryLifecycle.Failed | error, an InquiryError |
InquiryUiModel also carries view (the current screen), loading, busy, chrome (the
localized texts of the flow), error (an InquiryErrorView with text, placement and
retryable), failure (the failure notice of an inquiry that
cannot go on, which the UI shows instead of any step) and identity (the VerifiedIdentity of a
successful government ID verification). See Inquiry store and
Errors.
Links and the terminal redirect
The SDK validates and normalizes every link URL of a step and the redirect URL of a finished
inquiry: HTTPS://Example.COM becomes https://example.com/, and a space in a query becomes
%20. A URL that does not parse fails with InquiryError.InvalidLink, whose url is the rejected
value.
The inquiry UI opens every link, a LinkTarget.External, LinkTarget.Universal or
LinkTarget.Webview alike, with an Intent.ACTION_VIEW intent for its URL. Android resolves the
intent as it resolves any link: an app that handles the URL, such as your own app through Android
App Links, or otherwise the browser. When no app can open the URL, the UI sends
InquiryIntent.LinkRefused(url) and shows the error the store projects, a banner with the localized
error text.
When the inquiry ends, the terminal stage carries an optional redirect in
StageContent.Terminal.redirect, a LinkTarget.Universal. Once loading is false, the inquiry
UI opens the redirect and then dispatches InquiryAction.Close, so present finishes the activity
after the redirect opens. Without a redirect it dispatches Close at once. When no app can open
the redirect, the UI sends LinkRefused instead and shows the error; its close button dispatches
Close.
Activity recreation
The store and the flow are not saved in the activity's instance state:
- The runtime from
rememberFolioSdklives in aViewModelof theViewModelStoreOwnerand survives the recreation, as long asconfigstays equal andmapperstays the same instance. - A store kept with
rememberlives as long as the composition that holds it. presentsets the content of one activity instance; a recreated instance shows nothing from the inquiry until you callpresentagain.
To keep rotation from recreating the activity that hosts the inquiry, declare
android:configChanges="orientation|screenSize|keyboardHidden" on that activity.
Selfie engine
The inquiry UI captures selfies through the SelfieCaptureProvider of the InquiryPlatform you
pass to Host or present. id.folio:sdk ships DefaultSelfieCaptureProvider
(id.folio.sdk.inquiry.ui.verification.selfie), which the InquiryPlatform uses by default:
val platform = InquiryPlatform(nfc = runtime)While the step is being checked, or when the step has no liveness service URL, the selfie step shows its processing view and captures nothing.
DefaultSelfieCaptureProvider runs the capture engine of the selfie step in server-side mode: it
carries no license of its own and receives the service address and request headers from the SDK
for each capture. It runs passive or active liveness as the inquiry asks, skips the engine's
onboarding and success screens, shows a close button, does not offer a camera switch and hides the
engine's copyright line. It returns the captured image as JPEG.
Custom engine
To use another liveness engine, implement the interface from
id.folio.sdk.inquiry.ui.verification.selfie:
interface SelfieCaptureProvider {
suspend fun capture(context: Context, capture: SelfieCaptureView): SelfieCaptureResult
}SelfieCaptureView field | Type | Meaning |
|---|---|---|
captureSessionId | String | The capture session the liveness check belongs to |
mode | SelfieLivenessMode | PASSIVE or ACTIVE |
serviceUrl | String? | The liveness service address |
headers | List<HeaderView> | Request headers, each with name and value |
texts | SelfieCaptureTexts | The localized texts the engine shows |
SelfieCaptureTexts has the optional texts holdSteady, turnHead, lookStraight, centerFace
and processing. The UI calls the provider only with a serviceUrl.
Return a SelfieCaptureResult(image, transactionId): image is the captured image as a
ByteArray, transactionId the liveness transaction id. The inquiry submits the transaction id and
moves on. Throw an exception to give up: the inquiry goes back one step. A cancelled coroutine is
passed through as a cancellation.
NFC
Passport and ID card chip reading runs inside the inquiry flow through MrtdStore. It needs the
device's NFC adapter to be on, and it reads through the activity that is resumed at that moment. See
NFC.
Logging
The console writer of FolioSdkConfig.logging writes to logcat with the tag folio:
LogWriterConfig.Consoleselects the lowest console level written:TRACE,DEBUG,INFO,WARNorERROR, asLogLevelvalues. They map toLog.v,Log.d,Log.i,Log.wandLog.e.- An entry longer than 3000 characters arrives as several logcat entries prefixed
[0],[1]and so on. Join them in index order. FolioSdk.loggingConfig(consoleLevel)adds a console writer at that level to the file writer of the log journal; withnullthe SDK writes the journal only and nothing reaches logcat. Each runtime configures its own writers.
See Logging.
Shrinking
id.folio:sdk ships consumer ProGuard rules that R8 applies to your app automatically:
-keep class id.folio.sdk.** { *; }You add no rules for the SDK yourself.
Errors
FolioSDKExceptionis thrown bymountanddispatchof a store.FolioSdkStartup.Failed.errorcarries the runtime creation failure as aThrowable;FolioSdk.createthrows it.- Inquiry failures arrive as
InquiryLifecycle.Failedwith anInquiryError, asInquiryUiModel.errorfor errors a step shows, and asInquiryUiModel.failurefor an inquiry that cannot go on. An inquiry whose selfie photo step has no capture field fails withInquiryError.MissingCaptureField, whosestepis the step id.
See Errors.
Minimal app
A minimal host of the inquiry: the application initializes the platform, the activity creates the runtime and shows the inquiry for one launch code with the default selfie engine.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val config = FolioSdkConfig(
baseUrl = "https://app.folio.mobi",
organization = ORGANIZATION,
logging = FolioSdk.loggingConfig(null),
)
setContent {
MaterialTheme {
when (val startup = rememberFolioSdk(config, SavedIdentityMapper)) {
FolioSdkStartup.Starting -> Unit
is FolioSdkStartup.Failed -> {
LaunchedEffect(startup) {
Log.e(TAG, "runtime creation failed", startup.error)
}
}
is FolioSdkStartup.Ready -> Inquiry(startup.runtime)
}
}
}
}
@Composable
private fun Inquiry(runtime: FolioSdk) {
val store = remember(runtime) {
FolioInquiry.open(runtime, InquiryEntry.Start(LAUNCH_CODE))
}
DisposableEffect(store) {
onDispose { store.close() }
}
val model by store.state.collectAsState()
LaunchedEffect(model.lifecycle) {
if (model.lifecycle is InquiryLifecycle.Closed) finish()
}
val platform = remember(runtime) {
InquiryPlatform(nfc = runtime)
}
FolioInquiry.Host(store, platform)
}
private companion object {
const val TAG = "InquiryExample"
const val ORGANIZATION = "<organization>"
const val LAUNCH_CODE = "paste-a-launch-code-from-your-backend-here"
}
}The SDK imports this sample uses:
import id.folio.sdk.FolioSdk
import id.folio.sdk.FolioSdkConfig
import id.folio.sdk.FolioSdkStartup
import id.folio.sdk.InquiryEntry
import id.folio.sdk.InquiryLifecycle
import id.folio.sdk.inquiry.FolioInquiry
import id.folio.sdk.inquiry.InquiryPlatform
import id.folio.sdk.rememberFolioSdkKnown limits
XPlatform.initruns once per process; a second call throws.- Biometric prompts and passkey requests need a resumed
FragmentActivity. They fail when the resumed activity is a plainComponentActivity. - The native library covers
arm64-v8a,armeabi-v7aandx86_64only. - An internal fault inside an SDK call surfaces as a JVM exception; a fault outside an SDK call ends the process.