Skip to main content

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, an Activity you control for the login screen. Biometric tier additionally requires the login Activity to be a FragmentActivity.


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 http during development, also set android: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 (:sdk module or AAR + transitive deps).
  • Permissions in the manifest; runtime request wired (best‑effort).
  • Login Activity is a FragmentActivity; foreground activity exposed via a holder.
  • dispatchTouchEvent forwards to GkasTouchTracker.feed(...).
  • GuardianKeyGkas.create(...) built with backend URL + PlatformContext.
  • Login calls fetchConfig → buildSolution → login and handles the decision.

See the API reference for the full public API and the backend /config and /login contract.