> ## Documentation Index
> Fetch the complete documentation index at: https://docs.revrag.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Android Native

> Android SDK Integration Guide - Voice-enabled AI agent with real-time communication capabilities (Kotlin / Jetpack Compose)

# Android Integration Guide

| Requirement         | Value                                               |
| ------------------- | --------------------------------------------------- |
| SDK version         | **1.0.8**                                           |
| Min Android version | API 24 (Android 7.0)                                |
| Language            | Kotlin                                              |
| UI toolkit          | Jetpack Compose                                     |
| Build system        | Gradle (Kotlin DSL)                                 |
| Network             | Internet access at runtime                          |
| Maven repo          | `mavenCentral()` (already in every Android project) |

Get your API key from [https://app.revrag.ai](https://app.revrag.ai).

***

## Table of Contents

1. [Requirements](#requirements)
2. [Step 1 — Add the dependency](#step-1--add-the-dependency)
3. [Step 2 — Manifest permissions](#step-2--manifest-permissions)
4. [Step 3 — Initialize the SDK](#step-3--initialize-the-sdk)
5. [Step 4 — Identify the user](#step-4--identify-the-user)
6. [Step 5 — Add the floating button](#step-5--add-the-floating-button)
7. [Routing scenarios](#routing-scenarios)
8. [Button visibility control](#button-visibility-control)
9. [Voice calls & widget control](#voice-calls--widget-control)
10. [Call behavior](#call-behavior)
11. [Events](#events)
12. [Analytics helpers](#analytics-helpers)
13. [Observing all SDK events](#observing-all-sdk-events)
14. [Cleanup on logout](#cleanup-on-logout)
15. [Configuration reference](#configuration-reference)
16. [Troubleshooting](#troubleshooting)
17. [Pre-ship checklist](#pre-ship-checklist)

***

## Requirements

| Requirement  | Value                                               |
| ------------ | --------------------------------------------------- |
| Min OS       | Android 7.0 (API 24)                                |
| Language     | Kotlin 2.0+ (2.3+ if you use Ktor 3.4+ in your app) |
| UI framework | Jetpack Compose                                     |
| Build system | Gradle (Kotlin DSL)                                 |

<Tip>
  **Kotlin version:** The SDK is compiled with Kotlin 2.0.21, which emits Kotlin metadata 2.x. Consumers on Kotlin 1.9 or below will see `Class 'X' was compiled with an incompatible version of Kotlin` — bump your project to Kotlin 2.0+. If your own app pulls in Ktor 3.4+, its transitive stdlib forces Kotlin 2.3+.

  **HTTP client isolation:** The SDK bundles a private, relocated copy of Ktor under `ai.revrag.shaded.ktor.*`. You can use **any version of Ktor** (or none at all) in your own app without conflict — past, present, or future.
</Tip>

***

## Step 1 — Add the dependency

In your **module-level** `build.gradle.kts`:

```kotlin theme={null}
dependencies {
    implementation("ai.revrag:embed-android:1.0.8")
}
```

Sync Gradle. No extra repository setup needed — `mavenCentral()` is already in every Android project.

The SDK transitively pulls in:

* LiveKit Android SDK (WebRTC)
* Lottie for Android
* Coil (image loading)
* Kotlin Coroutines

<Tip>
  **Public package surface:** import everything from `ai.revrag.embed.android`. The one exception is `EmbedButtonInset` (button positioning), which lives in `ai.revrag.embed.android.ui`. Other internal packages may change without notice.
</Tip>

***

## Step 2 — Manifest permissions

The SDK's `AndroidManifest.xml` declares these permissions — they are **auto-merged** into your app via manifest merger. **You do not need to add them manually.**

```xml theme={null}
<!-- Auto-added by the SDK via manifest merger -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
```

<Tip>
  `RECORD_AUDIO` and `BLUETOOTH_CONNECT` (API 31+) are runtime permissions. The SDK requests them automatically when the user taps the call button — you do not trigger them yourself.
</Tip>

***

## Step 3 — Initialize the SDK

Create an `Application` class and call `initialize()` in `onCreate()`. This must run **once**, as early as possible.

```kotlin theme={null}
// MyApplication.kt
import android.app.Application
import ai.revrag.embed.android.EmbedSDK

class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        EmbedSDK.initialize(
            context  = this,
            apiKey   = "YOUR_REVRAG_API_KEY",
            embedUrl = null   // optional — custom backend URL; leave null for default
        ) { result ->
            if (result.success) Log.d("Revrag", "SDK ready")
            else                Log.e("Revrag", "Init failed: ${result.error}")
        }
    }
}
```

Register the Application class in `AndroidManifest.xml`:

```xml theme={null}
<application
    android:name=".MyApplication"
    ... >
```

<Tip>
  **What `initialize()` does:**

  1. Calls `GET /embedded-agent/initialize` with your API key
  2. Downloads and caches your widget configuration (colors, agent name, avatar)
  3. Pre-warms the Lottie animation cache to avoid a first-render flash
  4. Pre-fetches static icon assets via Coil
  5. Sets `EmbedSDK.isInitialized = true` — the button appears automatically once this is true
</Tip>

***

## Step 4 — Identify the user

Call this right after your login flow completes:

```kotlin theme={null}
import ai.revrag.embed.android.EmbedSDK
import ai.revrag.embed.android.EventKeys

EmbedSDK.event(
    EventKeys.USER_DATA,
    mapOf(
        "app_user_id" to "user_123",       // required
        "name"        to "Jane Doe",        // optional
        "email"       to "jane@email.com"   // optional
    )
)
```

<Warning>
  Send `USER_DATA` **before** the user taps the call button. Without `app_user_id`, the agent cannot identify the user and conversation context will not be attributed.
</Warning>

***

## Step 5 — Add the floating button

Wrap your root composable with `EmbedProvider.attach()` in `MainActivity`. This overlays the draggable button on top of all your existing Compose content automatically.

```kotlin theme={null}
// MainActivity.kt
import ai.revrag.embed.android.EmbedProvider
import androidx.activity.ComponentActivity

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        EmbedProvider.attach(
            activity   = this,
            appUserId  = "user_123",
            appVersion = BuildConfig.VERSION_NAME
        ) {
            // Your existing root composable — no changes needed
            MyAppTheme {
                AppNavigation()
            }
        }
    }
}
```

### Alternative — `EmbedProviderComposable`

If you already call `setContent {}` yourself or use a multi-Activity architecture:

```kotlin theme={null}
setContent {
    MyAppTheme {
        Box(modifier = Modifier.fillMaxSize()) {
            YourMainScreen()

            EmbedProviderComposable(
                currentScreen = "HomeScreen",
                appUserId     = "user_123",
                appVersion    = BuildConfig.VERSION_NAME
            )
        }
    }
}
```

<Warning>
  `EmbedProviderComposable` must be placed at the **root** of the Compose tree so `BoxWithConstraints` receives full-screen bounds. If it's nested inside a sized container the button will be stuck at the top-left corner.
</Warning>

### `EmbedProvider.attach()` props

| Parameter          | Type                          | Default         | Description                                          |
| ------------------ | ----------------------------- | --------------- | ---------------------------------------------------- |
| `appUserId`        | `String`                      | `""`            | Identifies the logged-in user                        |
| `appVersion`       | `String`                      | `"1.0.0"`       | Your app's version string — included in all events   |
| `accentColor`      | `Color`                       | `#6C63FF`       | Fallback gradient color before config loads          |
| `visibilityConfig` | `EmbedButtonVisibilityConfig` | show everywhere | Screen allow/exclude list and groups                 |
| `navController`    | `NavController?`              | `null`          | Enables automatic Jetpack Navigation screen tracking |

***

## Routing scenarios

Screen tracking lets the agent know which screen the user is on. Choose the scenario that matches your navigation setup.

***

### Scenario A — Single NavHost (most common)

Pass the `NavController` to `EmbedProvider`. The SDK tracks screen changes automatically — no additional code required.

```kotlin theme={null}
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        EmbedProvider.attach(
            activity   = this,
            appUserId  = "user_123",
            appVersion = BuildConfig.VERSION_NAME
        ) {
            val navController = rememberNavController()

            NavHost(navController = navController, startDestination = "home") {
                composable("home")         { HomeScreen() }
                composable("search")       { SearchScreen() }
                composable("product/{id}") { ProductScreen() }
                composable("cart")         { CartScreen() }
                composable("checkout")     { CheckoutScreen() }
                composable("profile")      { ProfileScreen() }
            }
        }
    }
}
```

Screen names reported to Revrag are the route strings: `"home"`, `"search"`, `"product/{id}"`, etc.

***

### Scenario B — Bottom navigation with multiple NavHosts

Each tab has its own `NavController`. Pass the **active** one to `EmbedProvider`:

```kotlin theme={null}
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        EmbedProvider.attach(
            activity   = this,
            appUserId  = "user_123",
            appVersion = BuildConfig.VERSION_NAME
        ) {
            val homeNavController    = rememberNavController()
            val exploreNavController = rememberNavController()
            val profileNavController = rememberNavController()

            var selectedTab by remember { mutableStateOf(0) }

            Scaffold(
                bottomBar = {
                    BottomNavBar(
                        selected = selectedTab,
                        onSelect = { selectedTab = it }
                    )
                }
            ) { padding ->
                Box(Modifier.padding(padding)) {
                    when (selectedTab) {
                        0 -> TabNavHost(navController = homeNavController,    startRoute = "home")
                        1 -> TabNavHost(navController = exploreNavController, startRoute = "explore")
                        2 -> TabNavHost(navController = profileNavController, startRoute = "profile")
                    }
                }
            }
        }
    }
}
```

<Tip>
  The SDK reports whichever screen is active inside the currently visible tab's NavHost.
</Tip>

***

### Scenario C — No NavController (manual screen tracking)

If you manage navigation yourself (custom back stack, Fragments, or plain Compose without NavController), fire `SCREEN_STATE` events manually:

```kotlin theme={null}
// Call this whenever the screen changes
EmbedSDK.event(
    EventKeys.SCREEN_STATE,
    mapOf("screen" to "ProductDetail", "action" to "enter")
)
```

**In a Fragment:**

```kotlin theme={null}
class ProductFragment : Fragment() {
    override fun onResume() {
        super.onResume()
        EmbedSDK.event(EventKeys.SCREEN_STATE, mapOf("screen" to "ProductDetail"))
    }
}
```

**In a Compose screen without NavController:**

```kotlin theme={null}
@Composable
fun CheckoutScreen() {
    LaunchedEffect(Unit) {
        EmbedSDK.event(EventKeys.SCREEN_STATE, mapOf("screen" to "Checkout"))
    }
    // your content
}
```

***

### Scenario D — Nested navigation (checkout flow inside tabs)

```kotlin theme={null}
NavHost(navController = rootNavController, startDestination = "main") {

    // Tab container
    composable("main") {
        MainTabsScreen()
    }

    // Checkout flow — nested graph
    navigation(startDestination = "cart", route = "checkout_flow") {
        composable("cart")         { CartScreen() }
        composable("payment")      { PaymentScreen() }
        composable("confirmation") { ConfirmationScreen() }
    }

    // Settings — separate stack
    composable("settings") { SettingsScreen() }
}
```

The SDK automatically picks up `"cart"`, `"payment"`, `"confirmation"`, etc. as the screen names from the nested graph.

***

## Button visibility control

By default the button shows on **every screen**. Use `EmbedButtonVisibilityConfig` to control this.

### Show only on specific screens

```kotlin theme={null}
EmbedProvider.attach(
    activity         = this,
    appUserId        = "user_123",
    appVersion       = BuildConfig.VERSION_NAME,
    visibilityConfig = EmbedButtonVisibilityConfig(
        allowedScreens  = listOf("home", "product", "cart"),
        excludedScreens = listOf("login", "splash", "onboarding")
    )
) { /* content */ }
```

### Keep button visible across a flow (e.g. checkout)

Use a group with `.CONTINUOUS` continuity so the button doesn't flash between screens:

```kotlin theme={null}
visibilityConfig = EmbedButtonVisibilityConfig(
    groups = listOf(
        EmbedButtonGroupConfig(
            id          = "checkout-flow",
            screens     = listOf("cart", "payment", "confirmation"),
            continuity  = EmbedButtonContinuity.CONTINUOUS,
            delayMs     = 500L,
            delayPolicy = EmbedButtonDelayPolicy.ONCE_PER_GROUP_ENTRY
        )
    )
)
```

### Visibility rules (evaluated in priority order)

```
1. Screen is in excludedScreens               →  always HIDDEN  (strongest rule)
2. allowedScreens and groups are both empty   →  always VISIBLE
3. Screen is in allowedScreens or any group   →  VISIBLE
4. Otherwise                                  →  HIDDEN
```

***

## Voice calls & widget control

The floating button (FAB) and its call card handle everything on their own. These functions let you drive the **same** widget programmatically — start or end a call, check call state, and collapse or observe the card — without waiting for a user tap.

### Call control

```kotlin theme={null}
// Identity comes from the stored app_user_id — send USER_DATA first.
EmbedSDK.event(EventKeys.USER_DATA, mapOf("app_user_id" to "user_123"))

// Start a call: handles mic + Bluetooth permission, connects, installs the reconnect provider.
EmbedSDK.startCall(activity) { started ->
    // true once the call started; false if it could not.
    // Failure reasons are reported to Revrag (analytics "error"), not returned here.
}

EmbedSDK.endCall()        // End the active call. Emits AGENT_DISCONNECTED.
EmbedSDK.isCallActive()   // Boolean — is a call currently connected?
```

<Warning>
  `startCall` returns `false` if no `app_user_id` is stored. Always send a `USER_DATA` event before starting a call.
</Warning>

### Widget control

```kotlin theme={null}
EmbedSDK.collapseWidget()  // Collapse the card. Fires agent_tap_to_close. Does NOT end a call.
EmbedSDK.isWidgetExpanded  // StateFlow<Boolean> — reactive expanded/collapsed state.
```

<Tip>
  Expanding is not exposed — the widget opens on a user tap or via a dashboard-configured `auto_trigger`. Your app can collapse it and observe its state.
</Tip>

***

## Call behavior

Behavior to know about while a call is active:

| Situation                               | What happens                                                                                                                     |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| App sent to **background**              | The active call **auto-disconnects** (the mic is unavailable in the background).                                                 |
| Navigate to an **excluded screen**      | The widget hides and the active call **ends**.                                                                                   |
| **Bluetooth** headset connects mid-call | Audio routes to the headset automatically.                                                                                       |
| Bluetooth **disconnects** mid-call      | Audio falls back to the **speaker** (not the earpiece).                                                                          |
| Call connects                           | After a configurable delay, the card auto-collapses to the button (the call keeps running) and shows a pulsing "live call" halo. |
| Agent ends the call                     | The call ends cleanly and does **not** reconnect.                                                                                |

***

## Events

### EventKeys reference

| Key              | Value            | Auto-fired                | Host-callable | Notes                                                       |
| ---------------- | ---------------- | ------------------------- | ------------- | ----------------------------------------------------------- |
| `USER_DATA`      | `user_data`      | No                        | **Yes**       | Send after login; must include `app_user_id`                |
| `SCREEN_STATE`   | `screen_state`   | **Yes** — `EmbedProvider` | Yes           | Fired on every screen enter and exit                        |
| `ANALYTICS_DATA` | `analytics_data` | **Yes** — SDK UI          | **Yes**       | SDK fires built-in events; host may also fire custom events |
| `CUSTOM_EVENT`   | `custom_event`   | No                        | Yes           | Free-form host-app events                                   |

### Sending events

```kotlin theme={null}
// User identity (required before first call)
EmbedSDK.event(EventKeys.USER_DATA, mapOf("app_user_id" to "user_123"))

// Custom analytics event
EmbedSDK.event(EventKeys.ANALYTICS_DATA, mapOf("event_name" to "checkout_started"))

// Manual screen state
EmbedSDK.event(EventKeys.SCREEN_STATE, mapOf("screen" to "CheckoutScreen", "action" to "enter"))
```

### Listen for agent call events

```kotlin theme={null}
// Register in onCreate or onStart
val onConnected: AgentEventCallback = { _ ->
    // e.g. pause background music
    Log.d("Revrag", "Agent call started")
}
val onDisconnected: AgentEventCallback = { payload ->
    val duration = (payload["metadata"] as? Map<*, *>)?.get("callDuration") as? Int ?: 0
    Log.d("Revrag", "Call lasted ${duration}s")
}

EmbedSDK.onAgent(AgentEvent.AGENT_CONNECTED,    onConnected)
EmbedSDK.onAgent(AgentEvent.AGENT_DISCONNECTED, onDisconnected)

// Always de-register in onDestroy
override fun onDestroy() {
    super.onDestroy()
    EmbedSDK.offAgent(AgentEvent.AGENT_CONNECTED,    onConnected)
    EmbedSDK.offAgent(AgentEvent.AGENT_DISCONNECTED, onDisconnected)
}
```

### Agent lifecycle events reference

| Event                   | Payload                                               | Use case                              |
| ----------------------- | ----------------------------------------------------- | ------------------------------------- |
| `AGENT_CONNECTED`       | `timestamp`, `metadata.callDuration: 0`, `server_url` | Pause background audio, start a timer |
| `AGENT_DISCONNECTED`    | `timestamp`, `metadata.callDuration` (seconds)        | Resume audio, log call length         |
| `POPUP_MESSAGE_VISIBLE` | —                                                     | Track tooltip impressions             |

### Listen to data events locally

Use `on()` / `off()` to observe any `EventKeys` event locally — for example to mirror `USER_DATA` into your own state. Pass the **same** callback reference to `off()` when removing it.

```kotlin theme={null}
val cb: EventCallback = { data -> Log.d("Revrag", "User data: $data") }
EmbedSDK.on(EventKeys.USER_DATA, cb)
EmbedSDK.off(EventKeys.USER_DATA, cb)
```

***

## Analytics helpers

These helpers fire `ANALYTICS_DATA` events with standardized payloads.

```kotlin theme={null}
// Track a custom event
EmbedSDK.trackEvent("product_viewed", mapOf("product_id" to "SKU-123"))

// Track a form interaction
EmbedSDK.trackFormEvent(
    formId    = "checkout_form",
    eventType = "submit",
    formData  = mapOf("step" to "payment")
)

// Track a rage-click
EmbedSDK.trackRageClick(
    elementId   = "btn_add_to_cart",
    coordinates = Pair(x, y),
    clickCount  = 5,
    elementType = "Button"
)

// Check microphone permission status
EmbedSDK.checkPermissions(context) { granted ->
    if (!granted) showMicEducationDialog()
}
```

### Events auto-fired by the SDK

| `event_name`                  | Trigger                                                            |
| ----------------------------- | ------------------------------------------------------------------ |
| `agent_tap_to_open`           | User taps the collapsed FAB                                        |
| `agent_tap_to_close`          | User taps avatar to close the card                                 |
| `agent_visible`               | `EmbedProvider` show-delay completes                               |
| `popup_message_visible`       | Inactivity nudge fires the tooltip                                 |
| `gen_tool_triggered`          | First data-channel message received                                |
| `agent_conversation_started`  | LiveKit `connect()` succeeds                                       |
| `microphone_permission_allow` | On every call start (`status: already_granted` or `newly_granted`) |

### Matching event names with `EmbedAnalyticsEvents`

The SDK fires its built-in events as `ANALYTICS_DATA` carrying an `event_name`. Match against these constants instead of hardcoding strings:

```kotlin theme={null}
EmbedAnalyticsEvents.GEN_TOOL_TRIGGERED          // "gen_tool_triggered"
EmbedAnalyticsEvents.POPUP_MESSAGE_VISIBLE       // "popup_message_visible"
EmbedAnalyticsEvents.AGENT_TAP_TO_OPEN           // "agent_tap_to_open"
EmbedAnalyticsEvents.AGENT_TAP_TO_CLOSE          // "agent_tap_to_close"
EmbedAnalyticsEvents.AGENT_VISIBLE               // "agent_visible"
EmbedAnalyticsEvents.AGENT_CONVERSATION_STARTED  // "agent_conversation_started"
EmbedAnalyticsEvents.RAGE_CLICK                  // "rage_click"
EmbedAnalyticsEvents.MICROPHONE_PERMISSION_ALLOW // "microphone_permission_allow"
EmbedAnalyticsEvents.FORM_EVENT                  // "form_event"
EmbedAnalyticsEvents.ERROR                       // "error"
```

```kotlin theme={null}
EmbedSDK.on(EventKeys.ANALYTICS_DATA) { data ->
    when (data["event_name"]) {
        EmbedAnalyticsEvents.AGENT_TAP_TO_OPEN -> { /* widget opened */ }
        EmbedAnalyticsEvents.ERROR             -> { /* SDK error */ }
    }
}
```

### Utility methods

```kotlin theme={null}
EmbedSDK.setAppVersion("2.0.0")        // override the reported app version
EmbedSDK.setCurrentFlow("onboarding")  // tag events with a logical flow name
EmbedSDK.isInitialized()               // Boolean snapshot
EmbedSDK.isInitializedFlow             // StateFlow<Boolean> — collect in Composables
EmbedSDK.widgetConfig                  // StateFlow<WidgetConfig?> — reactive dashboard config
```

***

## Observing all SDK events

The SDK emits its own analytics / lifecycle events (widget taps, call start, mic permission, rage-clicks, tool triggers, errors…) — all as `ANALYTICS_DATA` events carrying an `event_name`. Drop this helper into your app to log them all in one place or forward them to your analytics.

### Usage

```kotlin theme={null}
// after EmbedSDK.initialize:
SdkLifecycleListener.register()                  // log every SDK event to Logcat
SdkLifecycleListener.register { name, data ->     // or handle them yourself
    myAnalytics.track(name, data)
}

// on teardown:
SdkLifecycleListener.unregister()
```

### Full helper

```kotlin theme={null}
import ai.revrag.embed.android.EmbedAnalyticsEvents
import ai.revrag.embed.android.EmbedSDK
import ai.revrag.embed.android.EventCallback
import ai.revrag.embed.android.EventKeys
import android.util.Log

/**
 * Listens to the lifecycle / analytics events the SDK fires on its own (widget taps,
 * call start, permission prompts, rage-clicks, etc.).
 *
 * All of these are emitted as `ANALYTICS_DATA` events carrying an `event_name`, so a single
 * `EmbedSDK.on(EventKeys.ANALYTICS_DATA)` listener receives them all — we dispatch by `event_name`.
 */
object SdkLifecycleListener {

    private const val TAG = "SdkLifecycle"

    /** Held so we can pass the SAME reference to off() when unregistering. */
    private var callback: EventCallback? = null

    /**
     * Start listening. Idempotent — calling twice keeps a single registration.
     * @param onEvent optional handler for every event (name + full payload); defaults to logging.
     */
    fun register(onEvent: ((name: String, data: Map<String, Any?>) -> Unit)? = null) {
        if (callback != null) return
        val cb: EventCallback = { data ->
            val name = data["event_name"] as? String ?: "unknown"
            Log.d(TAG, "${label(name)} → $data")
            onEvent?.invoke(name, data)
        }
        callback = cb
        EmbedSDK.on(EventKeys.ANALYTICS_DATA, cb)
        Log.d(TAG, "Registered — listening for SDK lifecycle events")
    }

    /** Stop listening. Safe to call when not registered. */
    fun unregister() {
        callback?.let { EmbedSDK.off(EventKeys.ANALYTICS_DATA, it) }
        callback = null
        Log.d(TAG, "Unregistered")
    }

    /** Human-readable label for a known SDK event name (falls back to the raw name). */
    private fun label(name: String): String = when (name) {
        EmbedAnalyticsEvents.AGENT_TAP_TO_OPEN           -> "🔵 Widget expanded (agent_tap_to_open)"
        EmbedAnalyticsEvents.AGENT_TAP_TO_CLOSE          -> "⚪ Widget collapsed (agent_tap_to_close)"
        EmbedAnalyticsEvents.AGENT_VISIBLE               -> "👁️ Widget became visible (agent_visible)"
        EmbedAnalyticsEvents.AGENT_CONVERSATION_STARTED  -> "🎙️ Call started (agent_conversation_started)"
        EmbedAnalyticsEvents.POPUP_MESSAGE_VISIBLE       -> "💬 Popup/tooltip shown (popup_message_visible)"
        EmbedAnalyticsEvents.MICROPHONE_PERMISSION_ALLOW -> "🎤 Mic permission granted (microphone_permission_allow)"
        EmbedAnalyticsEvents.GEN_TOOL_TRIGGERED          -> "🛠️ Agent tool triggered (gen_tool_triggered)"
        EmbedAnalyticsEvents.RAGE_CLICK                  -> "😡 Rage click (rage_click)"
        EmbedAnalyticsEvents.FORM_EVENT                  -> "📝 Form event (form_event)"
        EmbedAnalyticsEvents.ERROR                       -> "❌ SDK error (error)"
        else                                             -> "ℹ️ $name"
    }
}
```

***

## Cleanup on logout

Call this when the user logs out or switches accounts to prevent stale data from leaking into the next session.

```kotlin theme={null}
fun onUserLogout() {
    EmbedSDK.clearStorageCache()
}
```

***

## Configuration reference

### `EmbedButtonVisibilityConfig`

| Field             | Type                           | Default            | Description                                     |
| ----------------- | ------------------------------ | ------------------ | ----------------------------------------------- |
| `allowedScreens`  | `List<String>`                 | `[]` (all screens) | Button shows only on these screens              |
| `excludedScreens` | `List<String>`                 | `[]`               | Button always hidden on these screens           |
| `showDelay`       | `Long` ms                      | `0`                | Delay before button fades in on allowed screens |
| `groups`          | `List<EmbedButtonGroupConfig>` | `[]`               | Per-group override rules                        |
| `defaultInset`    | `EmbedButtonInset?`            | SDK default        | Default edge snap position                      |

### `EmbedButtonGroupConfig`

| Field         | Type                     | Default      | Description                                                              |
| ------------- | ------------------------ | ------------ | ------------------------------------------------------------------------ |
| `id`          | `String`                 | required     | Unique group identifier                                                  |
| `screens`     | `List<String>`           | required     | Screens that belong to this group                                        |
| `continuity`  | `EmbedButtonContinuity`  | `PER_SCREEN` | `CONTINUOUS` keeps the button mounted across group screens without flash |
| `inset`       | `EmbedButtonInset?`      | `null`       | Per-group snap position override                                         |
| `delayMs`     | `Long` ms                | `0`          | Show delay for this group                                                |
| `delayPolicy` | `EmbedButtonDelayPolicy` | `PER_SCREEN` | Controls when the delay resets                                           |

### `EmbedButtonDelayPolicy`

| Value                  | Behaviour                                          |
| ---------------------- | -------------------------------------------------- |
| `PER_SCREEN`           | Delay applies every time the screen becomes active |
| `ONCE_PER_GROUP_ENTRY` | Delay only on the first entry into a group         |
| `ONCE_PER_APP_SESSION` | Delay only on the very first view of the session   |

### `EmbedButton` direct props

| Parameter        | Type               | Default             | Description                               |
| ---------------- | ------------------ | ------------------- | ----------------------------------------- |
| `appUserId`      | `String`           | `""`                | User identifier passed to the voice agent |
| `accentColor`    | `Color`            | `#6C63FF`           | Fallback gradient                         |
| `isVisible`      | `Boolean`          | `true`              | Hides the button without unmounting it    |
| `containerInset` | `EmbedButtonInset` | right=24, bottom=80 | Snap position from screen edges           |

***

## Troubleshooting

### The button never appears

**Cause A — `initialize()` not called or failed**

Check Logcat for `[EmbedSDK]` tags:

```kotlin theme={null}
EmbedSDK.initialize(context, apiKey = "YOUR_KEY") { result ->
    Log.d("Revrag", "success=${result.success} error=${result.error}")
}
```

Fix: verify your API key has no leading/trailing spaces and the device has internet.

***

**Cause B — SDK not ready yet**

`initialize()` is async. The button is hidden until `isInitialized = true` and appears automatically — no action required. To observe readiness:

```kotlin theme={null}
lifecycleScope.launch {
    EmbedSDK.isInitializedFlow.collect { ready ->
        Log.d("Revrag", "SDK ready: $ready")
    }
}
```

***

**Cause C — Screen excluded by visibility config**

Check `excludedScreens` and `allowedScreens`. Screen names are **case-sensitive** and must match exactly what's in your NavGraph routes or what you pass to `SCREEN_STATE`.

```
"Home" ≠ "home"  — they must match exactly
```

***

### Upgrading from 1.0.7 → 1.0.8

Drop-in replacement. **No code changes required** — bump the version string only. 1.0.8 *adds* the host-callable call-control API (`startCall` / `endCall` / `isCallActive`), widget control (`collapseWidget` / `isWidgetExpanded`), and the `EmbedAnalyticsEvents` constants; all existing 1.0.7 code keeps working unchanged.

### Upgrading from 1.0.6 → 1.0.7

Drop-in replacement. **No code changes required** — bump the version string only.

If you were blocked on 1.0.6 by errors like:

```
NoClassDefFoundError: io.ktor.client.plugins.contentnegotiation.ContentNegotiation
```

…or any other Ktor classpath conflict, 1.0.7 fixes it. The SDK now ships its own Ktor under `ai.revrag.shaded.ktor.*`, so your app's Ktor version no longer matters.

***

### `Class 'X' was compiled with an incompatible version of Kotlin`

**Cause:** Your project is on Kotlin 1.9 or earlier. The SDK is compiled with Kotlin 2.0.21 and the K1 compiler cannot read Kotlin metadata 2.x.

Fix: bump your project's Kotlin version to **2.0+** in your root `build.gradle.kts`:

```kotlin theme={null}
plugins {
    kotlin("android") version "2.0.21" apply false
}
```

If your app also pulls in Ktor 3.4+, you'll need Kotlin **2.3+** (Ktor 3.4's transitive stdlib forces this).

***

### `Could not resolve ai.revrag:embed-android:1.0.8`

**Cause:** Dependency not cached, or stale Gradle cache.

```bash theme={null}
# Clear cache and retry
./gradlew --refresh-dependencies
```

Verify the artifact is live: `https://central.sonatype.com/artifact/ai.revrag/embed-android`

Confirm `mavenCentral()` is in your `settings.gradle.kts`:

```kotlin theme={null}
repositories {
    google()
    mavenCentral()   // ← must be present
}
```

***

### Button stuck at top-left corner

**Cause:** `EmbedProviderComposable` is not at the root of the Compose tree.

```kotlin theme={null}
// ❌ Wrong — nested inside a sized Box
Box(modifier = Modifier.size(200.dp)) {
    EmbedProviderComposable(...)
}

// ✅ Correct — use EmbedProvider.attach() which handles root placement automatically
EmbedProvider.attach(activity = this, appUserId = "user_123") {
    YourContent()
}
```

***

### Audio plays through speaker instead of Bluetooth headset

**Cause:** `BLUETOOTH_CONNECT` runtime permission denied (Android 12+).

Direct the user to app settings:

```kotlin theme={null}
EmbedSDK.checkPermissions(context) { granted ->
    if (!granted) {
        startActivity(
            Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
                data = Uri.fromParts("package", packageName, null)
            }
        )
    }
}
```

***

### Analytics events missing from dashboard

Check in this order:

1. `USER_DATA` was sent with a valid `app_user_id` before other events
2. `initialize()` completed successfully (`isInitialized == true`)
3. Device has internet connectivity
4. The SDK rate-limits to 5 req/s — bursts are **queued, not dropped**

***

### User identity leaks between accounts

Always call `clearStorageCache()` on logout before the next user logs in:

```kotlin theme={null}
fun onUserLogout() {
    EmbedSDK.clearStorageCache()
}
```

***

## Pre-ship checklist

### Basic setup

* [ ] `ai.revrag:embed-android:1.0.8` added and Gradle synced
* [ ] `Application` class created and registered in `AndroidManifest.xml`
* [ ] `EmbedSDK.initialize()` called in `Application.onCreate()`
* [ ] `USER_DATA` event sent with `app_user_id` immediately after login
* [ ] `EmbedProvider.attach()` wraps the root composable in `MainActivity`
* [ ] `clearStorageCache()` called on logout

### Screen tracking

* [ ] `NavController` passed to `EmbedProvider` (Scenario A/B), or `SCREEN_STATE` fired manually (Scenario C)
* [ ] Nested graphs tracked correctly (Scenario D if applicable)

### Visibility

* [ ] `visibilityConfig` configured if the button should not show on all screens
* [ ] Screen names in config match routes exactly (case-sensitive)

### Production readiness

* [ ] Error handling in `EmbedSDK.initialize()` `onResult` callback
* [ ] Agent event listeners registered **and** de-registered in `onDestroy`
* [ ] If driving calls from your own UI, `USER_DATA` is sent before `startCall()`
* [ ] Tested on a physical device — microphone permission dialog does not appear in emulator

***

## Support

* Issues: [GitHub Issues](https://github.com/revrag-ai/embed-android/issues)
* Docs: [Revrag Documentation](https://docs.revrag.ai)
* Dashboard: [app.revrag.ai](https://app.revrag.ai)
