Android Integration
🤖 Android Integration
This guide shows how to call the GuardianKey Mobile SDK at the login step of your own Android app — the same wiring used by the reference demo app. The SDK is written in Kotlin Multiplatform but on Android it is a normal AAR/library you consume from Kotlin or Java.
Requirements:
minSdk 26+, Kotlin, anActivityyou control for the login screen. Biometric tier additionally requires the loginActivityto be aFragmentActivity.
1. Add the SDK dependency
The SDK module is :sdk (artifact namespace io.guardiankey.gkas). Pick one of:
Option A — include the module (monorepo / source):
// settings.gradle.kts
include(":sdk")
// app/build.gradle.kts
dependencies {
implementation(project(":sdk"))
}
Option B — consume the AAR. Build it and publish to mavenLocal() (or drop
the .aar into a flat repo):
./gradlew :sdk:assembleRelease # produces sdk/build/outputs/aar/sdk-release.aar
# or, if the SDK is configured to publish:
./gradlew :sdk:publishToMavenLocal
Because the SDK relies on Ktor, coroutines and AndroidX Biometric, declare those transitive dependencies in your app when consuming a bare AAR:
dependencies {
implementation("io.ktor:ktor-client-okhttp:<version>")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:<version>")
implementation("androidx.biometric:biometric:<version>")
}
2. Declare permissions
Add the permissions the wide collectors use. All are optional at runtime —
if denied, the related signal simply reports available:false and login
continues.
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
If your backend talks over cleartext
httpduring development, also setandroid:usesCleartextTraffic="true"on<application>.
3. Host the login screen in a FragmentActivity
The biometric identity tier shows a BiometricPrompt, which needs a
FragmentActivity. Make your login Activity a FragmentActivity and expose
the currently resumed one to the SDK via a small holder.
// ActivityHolder.kt
object ActivityHolder {
private var ref = java.lang.ref.WeakReference<FragmentActivity>(null)
var current: FragmentActivity?
get() = ref.get()
set(value) { ref = java.lang.ref.WeakReference(value) }
}
class LoginActivity : FragmentActivity() {
override fun onResume() {
super.onResume()
ActivityHolder.current = this
}
override fun onPause() {
super.onPause()
if (ActivityHolder.current === this) ActivityHolder.current = null
}
// 4. Feed touch events to the behavioral collector (timing/geometry only).
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
if (ev != null) GkasTouchTracker.feed(ev)
return super.dispatchTouchEvent(ev)
}
}
4. Feed touch events (behavioral signal)
The SDK cannot observe touches globally, so the host app forwards raw events to
GkasTouchTracker.feed(...) — typically from Activity.dispatchTouchEvent, as
shown above. The tracker keeps only behavioral characteristics of the touches and
never captures typed content. This feeds the SDK's behavioral signal. If you
skip this step, that signal simply stays empty and login still works.
5. Request runtime permissions (best‑effort)
Ask for the dangerous permissions before/at the login screen. Don't block login on the result — denied permissions only degrade the corresponding signals.
private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { /* best-effort */ }
private fun requestCollectorPermissions() {
val perms = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
perms += Manifest.permission.BLUETOOTH_CONNECT
perms += Manifest.permission.BLUETOOTH_SCAN
}
permissionLauncher.launch(perms.toTypedArray())
}
6. Create the SDK instance
Build a GuardianKeyGkas with your backend base URL and a PlatformContext. The
activityProvider lets the biometric tier find the foreground activity.
import io.guardiankey.gkas.GkasSdkConfig
import io.guardiankey.gkas.GuardianKeyGkas
import io.guardiankey.gkas.PlatformContext
val gkas = GuardianKeyGkas.create(
config = GkasSdkConfig(
backendBaseUrl = "https://your-backend.example.com/api", // no trailing slash
httpTimeoutMs = 8000,
debug = BuildConfig.DEBUG,
),
platform = PlatformContext(
context = applicationContext,
activityProvider = { ActivityHolder.current },
),
)
For the Android emulator reaching a backend on the host machine, use
http://10.0.2.2:8080.
7. Drive the login
Call the three steps in order inside a coroutine (e.g. in a ViewModel). Map the
LoginResult to your navigation.
import io.guardiankey.gkas.LoginRequest
import io.guardiankey.gkas.LoginResult
fun login(username: String, password: String) = viewModelScope.launch {
try {
val config = gkas.fetchConfig() // 1) GET /config
val solution = gkas.buildSolution(config, username) // 2) collect + sign
val result = gkas.login( // 3) POST /login
LoginRequest(
username = username,
password = password,
gkasSolution = solution,
config = config,
)
)
when (result) {
is LoginResult.Allowed -> goToHome()
is LoginResult.Blocked -> showBlocked(result.reason) // stay on login
is LoginResult.Error -> showError(result.message)
}
} catch (e: Throwable) {
showError("Communication failure: ${e.message}")
}
}
That's the whole integration: fetchConfig → buildSolution → login, then react
to Allowed / Blocked / Error.
✅ Checklist
- SDK dependency added (
:sdkmodule or AAR + transitive deps). - Permissions in the manifest; runtime request wired (best‑effort).
- Login
Activityis aFragmentActivity; foreground activity exposed via a holder. -
dispatchTouchEventforwards toGkasTouchTracker.feed(...). -
GuardianKeyGkas.create(...)built with backend URL +PlatformContext. - Login calls
fetchConfig → buildSolution → loginand handles the decision.
See the API reference for the full public API and
the backend /config and /login contract.