# Android Copilot Integration > Master prompt for an AI coding agent (Copilot / Cursor / Claude Code) to integrate ai.revrag:embed-android end-to-end. URL: /embed/llmText/android-copilotintegration Markdown: /embed/llmText/android-copilotintegration.md # RevRag Android SDK: AI Agent Integration Master Prompt [#revrag-android-sdk-ai-agent-integration-master-prompt] > **How to use:** paste this entire document into your AI coding agent (Copilot, Cursor, > Claude Code, etc.) inside the Android app you want to integrate. The agent will inspect > your app, add and configure `ai.revrag:embed-android`, ask you the few decisions only you > can make, then validate the integration end-to-end and report. > > Every rule below is derived from the SDK's source (**v1.2.0**). Where this prompt and a > generic Android tutorial disagree, this prompt is correct for **this** SDK. This SDK is > for **native Android apps** (Kotlin/Java, Jetpack Compose or XML Views). React Native and > Flutter apps use `embed-react-native` and `embed_flutter` instead. *** You are an **expert Android integration agent**. Take this application from **zero → working → validated** RevRag SDK integration with minimal manual work. **Prime directives** 1. **Inspect first, modify second.** Never assume the AGP/Kotlin version, UI toolkit, navigation library, or Activity structure. Read the project. 2. **Preserve host behavior.** Merge into existing config. Never blindly overwrite `AndroidManifest.xml`, `settings.gradle(.kts)`, `build.gradle(.kts)`, the `Application` class, or any Activity. 3. **Never claim success without running the validation** in §19–§22 and observing the log lines and on-device behavior named there. 4. **Ask the developer** for the decisions in §12. Do not guess them. 5. **Never hide a crash or error.** Report SDK-side issues you cannot fix. 6. **Do not add dependencies, permissions, or config the SDK does not require.** In particular: no `BLUETOOTH_CONNECT`, no `SYSTEM_ALERT_WINDOW`, no ProGuard rules, no runtime-permission code, no Ktor/OkHttp/LiveKit dependencies of your own. 7. **Public API only.** Import from `ai.revrag.embed.android.*`. Never import `ai.revrag.embed.core.*`; it is internal and changes without notice. *** ## 1. Inspect the existing application [#1-inspect-the-existing-application] Record each of these before changing anything: | What | How to find it | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Toolchain | `gradle/libs.versions.toml`, root + `app/build.gradle(.kts)`: AGP, Kotlin, compileSdk, minSdk, Java/JVM target, `gradle/wrapper/gradle-wrapper.properties` | | UI toolkit | Compose (`setContent {}`, `buildFeatures.compose`), XML Views (`setContentView(R.layout…)`), or hybrid (`ComposeView` in XML / `AndroidView` in Compose) | | Activity structure | single Activity, or several Activities: list every one in the manifest, including splash, login, MPIN, deep-link and trampoline Activities | | Navigation | Compose `NavHost` + `NavController`, `NavHostFragment` (XML nav graph), manual `FragmentTransaction`s, Activity-per-screen, `ViewPager2`, bottom tabs, Compose state tabs inside one route, several NavHosts | | `Application` class | is there one, and is it registered with `android:name` on ``? | | Login gate | where auth succeeds, and where the user id becomes known; where logout happens | | Dialogs & sheets | `DialogFragment`, `BottomSheetDialogFragment`, Compose `Dialog` / `ModalBottomSheet`, full-screen dialog "screens" | | WebViews | any screen that hosts a `WebView` (KYC, DigiLocker, eSign, payment, partner pages) | | Third-party Activities | Custom Tabs, payment / OAuth / biometric SDK screens | | **Existing voice/WebRTC** | `grep -rn "livekit\|webrtc\|twilio\|agora\|zego" --include=*.gradle* --include=*.toml .`, which shows another VoIP stack (conflict risk, §22) | | Shared peers | Compose BOM version, `lottie` / `lottie-compose`, `navigation-compose`, `activity-compose` | | Process model | any `android:process` attribute (multi-process, §22) | | Release build | `isMinifyEnabled` / R8, `FLAG_SECURE` usage, an SLF4J binding (`logback-android`, `slf4j-android`) | | App version source | `versionName` in `defaultConfig`, or a CI-stamped value | **VALIDATE:** you can state, in one sentence each: the UI toolkit, the Activity structure, the navigation type, where the `Application` class is, where login and logout happen, and whether there are WebViews, dialogs, or third-party Activities. *** ## 2. Determine compatibility [#2-determine-compatibility] | Target | Floor | Notes | | ------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------- | | `minSdk` | **24** | Hard floor. | | `compileSdk` | **34** | Enforced by AAR metadata; the build fails with a clear message below it. | | AGP | **8.1.1** | Needed for compileSdk 34. | | Gradle | **8.0** | | | Kotlin | **2.0** | Kotlin 1.9 fails with "binary version of its metadata is 2.0.0"; there is no flag around it. | | JDK / JVM target | **17** | `compileOptions` + `kotlin { jvmToolchain(17) }`. | | Activities that host the widget | `ComponentActivity` | `AppCompatActivity` and `FragmentActivity` qualify. A plain `android.app.Activity` cannot host it. | | Process | single process | Only the process that calls `initialize` has an agent. | If any target is below its floor, explain it, raise it, and re-validate. **Do not upgrade or downgrade unrelated dependencies.** XML-only apps need **no** Compose toolchain (no compose compiler plugin, no `buildFeatures.compose`): the Compose runtime arrives as an ordinary library dependency. *** ## 3. Add the repositories and the SDK [#3-add-the-repositories-and-the-sdk] **`settings.gradle(.kts)`**: JitPack is **required**. LiveKit's `audioswitch` dependency is published only there. Without it sync fails with `Could not find com.github.davidliu:audioswitch`. ```kotlin dependencyResolutionManagement { repositories { google() mavenCentral() maven { url = uri("https://jitpack.io") } } } ``` **`app/build.gradle(.kts)`**: ```kotlin dependencies { implementation("ai.revrag:embed-android:1.2.0") } ``` **VALIDATE:** Gradle sync succeeds and `./gradlew :app:assembleDebug` passes **before** any SDK code is written. *** ## 4. Dependency conflicts [#4-dependency-conflicts] The SDK brings: LiveKit Android **2.27.0** (WebRTC), Compose BOM **2024.09.02**, `activity-compose`, `navigation-compose`, `lottie-compose` **6.4.0**, and Ktor (**shaded** into `ai.revrag.shaded.ktor`; it never appears in the host's dependency tree and needs no handling). ```bash ./gradlew :app:dependencies --configuration releaseRuntimeClasspath | grep -E "livekit|compose-bom|lottie|audioswitch" ``` Rules: * Gradle resolving a **higher** version is fine. Never `force` / `strictly` LiveKit **below 2.27.0**, because that breaks the voice transport. * A host on a newer Compose BOM may see a constraint conflict. Do not downgrade the host; let Gradle pick the highest. * A host that already ships **another WebRTC/VoIP SDK** (Twilio, Agora, an older LiveKit): stop and report it (§22). Two WebRTC stacks can clash at runtime and must be tested together. *** ## 5. Android manifest [#5-android-manifest] 1. **Permissions.** The AAR merges **nothing** into the host manifest, not even these. Add to `app/src/main/AndroidManifest.xml`, inside ``, above ``: ```xml ``` A missing `RECORD_AUDIO` is auto-denied by the OS with **no dialog**: the widget shows and every call fails. 2. **Do not add** `BLUETOOTH_CONNECT` (headset routing is OS-managed; declaring it causes a spurious "Nearby devices" prompt) or `SYSTEM_ALERT_WINDOW` (the SDK never uses one). 3. **Do not copy** `android:usesCleartextTraffic="true"` from RevRag example apps. They target a dev backend. Production is `https://embed.revrag.ai`. 4. **`Application` class** registered: ``. Create one if the app has none. 5. **LiveKit's transitive manifest entries.** The merged manifest will also contain `ACCESS_NETWORK_STATE`, `MODIFY_AUDIO_SETTINGS`, `BLUETOOTH` (maxSdk 30), `CAMERA`, `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_MEDIA_PROJECTION` and a `ScreenCaptureService`. The SDK never uses the camera or screen capture. **Ask** before removing any with `tools:node="remove"`, and re-test a call afterwards. **VALIDATE:** read the **merged** manifest, not the source one: `app/build/intermediates/merged_manifest/debug/processDebugMainManifest/AndroidManifest.xml` contains `INTERNET` and `RECORD_AUDIO`. *** ## 6. Build validation [#6-build-validation] ```bash ./gradlew :app:assembleDebug ./gradlew :app:assembleRelease # if the app has a release config ``` **VALIDATE:** both compile. Fix any error introduced by the integration here, not later. *** ## 7. SDK initialization (exact contract) [#7-sdk-initialization-exact-contract] Call `EmbedSDK.initialize` **once, in `Application.onCreate`**. Not in a post-login Activity: screens created before initialize never get the agent. ```kotlin import ai.revrag.embed.android.EmbedSDK class MyApp : Application() { override fun onCreate() { super.onCreate() EmbedSDK.initialize( context = this, apiKey = BuildConfig.REVRAG_API_KEY, // REQUIRED, from secure build config // embedUrl = "…", // OMIT for production; only for a RevRag-provided non-prod host // appVersion = "2.4.0", // OPTIONAL, see §9 ) { result -> if (!result.success) Log.e("Embed", "init failed: ${result.error}") } } } ``` * Signature: `initialize(context, apiKey, embedUrl = null, autoTrackActivities = true, onResult)`, plus an overload with `appVersion: String`. Use **named arguments**. * `onResult(InitResult)`: `InitResult(success: Boolean, error: String?)`. This is the only place init failure surfaces in a release build, so always log it. * Initialization is **fail-open**: the app keeps running; the widget simply won't appear until config arrives. * `autoTrackActivities = true` (default) lets the SDK follow Activities on its own. Leave it on. * Do **not** pass an `agentTriggerMode` here; there is none. Call mode is chosen per call (§17). **VALIDATE (debug build):** `adb logcat | grep RevragEmbed` shows, in order: `[RevragEmbed][Init] 🚀 Stage 1 — initialize() called` → `Stage 3 — calling GET /embedded-agent/initialize …` → `Stage 4 — /initialize response received` → `✅ Stage 5 — EmbedEvent configured, isInitialized = true` → `🎉 Initialization complete — SDK is ready`. Use `grep RevragEmbed`, **not** `logcat -s RevragEmbed`: init lines are printed under the `System.out` tag. *** ## 8. API key & environment validation [#8-api-key--environment-validation] * The key and the backend environment must be a **matching pair**. A dev key on the production backend (or the reverse) returns no widget config: the button never appears and no error is raised. * Never hardcode a key copied from RevRag's example apps; use the client's own key from secure build config (`BuildConfig` field / `local.properties` / CI secret). If none is available, **stop**, report "API key missing", and ask the developer. * A blank key fails init (`Stage 0 — apiKey must not be empty`). **VALIDATE:** `EmbedSDK.widgetConfig` (a `StateFlow`) becomes **non-null** after init. Do **not** judge readiness on `isInitializedFlow`: it flips true from having a key, before any network call. A null `widgetConfig` after a successful request means provisioning (wrong key/environment, or no config for this platform and app version). The SDK latches that answer until `initialize` runs again. *** ## 9. App version [#9-app-version] * **Optional.** Omitted or blank → the SDK reports the host's `versionName` automatically. Pass `appVersion = "x.y.z"` to `initialize` only when the developer needs a different reported value. An explicit init value wins over any later setter. * The widget config **and the avatar** are provisioned per platform **and per app version** on the RevRag dashboard. A version bump can come back with no config (widget disappears) or no avatar (generic animation). Tell the developer to re-verify on the dashboard at every version bump. **VALIDATE:** the dashboard has an Android widget config (and avatar) for exactly the version this build reports. *** ## 10. User identity (`app_user_id`) & logout [#10-user-identity-app_user_id--logout] No call can connect until the SDK knows the user. **After login**, send: ```kotlin import ai.revrag.embed.android.EmbedSDK import ai.revrag.embed.android.EventKeys EmbedSDK.event(EventKeys.USER_DATA, mapOf("app_user_id" to userId)) // stable id, required ``` or pass `appUserId = userId` to `attachOverlay(...)` (§13). For a no-auth/demo app, send it on the first screen. **On every logout:** ```kotlin EmbedSDK.clearStorageCache() ``` Otherwise one user's identity and context carry into the next user's session. * Without `app_user_id`, `startCall` is refused (debug log: `[StartCall] refused — app_user_id is not set — send an EventKeys.USER_DATA event carrying 'app_user_id', or pass appUserId to attachOverlay()`) and the UI-graph cache cannot upload. Required order: `initialize` → init success → `USER_DATA` → widget config → widget shown on an allowed screen → call. *** ## 11. Widget configuration (dashboard-driven) [#11-widget-configuration-dashboard-driven] Position, colors, avatar, collapsed view, media mode (audio / video / chat), nudges and action intelligence come from the **dashboard**, not host code. Malformed backend values fall back to defaults and never crash the host. | Case | Expected | | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Config exists | Widget appears on allowed screens; `agent_visible` analytics event fires. | | Config null | No widget; app fully functional. See §8 for the cause. | | Avatar missing for this app version | Widget shows a generic pulse animation instead of the client's avatar. | | Video tenant not provisioned | Permanent "Connecting avatar…" placeholder during a call. | | Slow first launch | HTTP timeout 15 s; init retries 3× (\~47 s), then refetches on the next screen change. The widget is late, not lost. | If the developer does **not** want the agent to act on screens (tap/type/scroll), that is a dashboard switch: `action_config.flags.actionIntelligence = false`. There is no client flag. *** ## 12. Ask the developer (do not guess) [#12-ask-the-developer-do-not-guess] After inspecting, **list the actual screen names the SDK will report** (§14: route patterns, nav labels, Fragment / Activity class names), then ask: 1. **Which screens should the widget appear on?** → `allowedScreens` (empty = everywhere). 2. **Which screens must never show it** (login, OTP, MPIN, payment, regulated data)? → `excludedScreens`. Entering one **ends** a live call. 3. **Which journeys must keep a live call alive across screens?** → one `CONTINUOUS` group per journey (§15). Two allowed screens **not** in the same continuous group end the call on every hop between them. 4. **Should the widget appear after a delay on some screens?** → `showDelay` / group `delayMs` * `EmbedButtonDelayPolicy`. 5. **Any bottom bar, host FAB, or sticky CTA the widget would cover?** → `defaultInset` / group `inset`. 6. **WebView screens** with several pages: should each page be its own screen on the backend? (→ `setSubScreen`, §14.4.) 7. **Who starts calls:** only the user via the widget, or also the app (§17)? Co-pilot or full avatar (`WORKFLOW`) experience? 8. **Is a call ending acceptable** when the user backgrounds the app, opens Custom Tabs / a payment SDK, or leaves for an excluded screen? (It always does; confirm they accept it.) 9. **Compliance:** `FLAG_SECURE` screens? Corporate network / VPN users? Minified release? 10. **App version source** if `versionName` is not what should be reported; **API key location**. Wait for answers before writing screen configuration. *** ## 13. Mount the widget [#13-mount-the-widget] **Exactly one mount mechanism in the whole app**: `attachOverlay` **or** `EmbedProviderComposable`, never both (two mounts = two widgets that drift apart). **No teardown**: never call `detach()` in `onPause` / `onStop` / `onDestroy`. ### 13.1 Any app (recommended: works for XML, Compose, multi-Activity) [#131-any-app-recommended-works-for-xml-compose-multi-activity] In `Application.onCreate`, after `initialize`: ```kotlin registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { override fun onActivityResumed(activity: Activity) { val host = activity as? ComponentActivity ?: return EmbedProvider.attachOverlay( activity = host, appUserId = Session.userIdOrEmpty(), visibilityConfig = EMBED_VISIBILITY, // one shared val, §15 ) } override fun onActivityCreated(a: Activity, b: Bundle?) = Unit override fun onActivityStarted(a: Activity) = Unit override fun onActivityPaused(a: Activity) = Unit override fun onActivityStopped(a: Activity) = Unit override fun onActivitySaveInstanceState(a: Activity, b: Bundle) = Unit override fun onActivityDestroyed(a: Activity) = Unit }) ``` * `attachOverlay` is **idempotent** per Activity: the first call mounts, later calls move the widget or update the screen. Arguments on repeat calls are **ignored**: `visibilityConfig`, `accentColor`, `appUserId` are fixed at first mount. Define them once. * It must run **after** `setContentView`. `onResume` always satisfies this. * `accentColor` is a `@ColorInt Int`, not a Compose `Color`. * Keep the returned `EmbedOverlayHandle` if you need `setCurrentScreen` / `setSubScreen`. * **Never** use `EmbedProvider.attach(...)` on an XML Activity: it calls `setContent` and wipes the View hierarchy. ### 13.2 Compose single-Activity app with a `NavController` [#132-compose-single-activity-app-with-a-navcontroller] Attach **inside `setContent`**, keyed on the controller, so route tracking starts on the first frame (from `onCreate` the controller would not exist yet): ```kotlin setContent { val navController = rememberNavController() var embedHandle by remember { mutableStateOf(null) } LaunchedEffect(navController) { embedHandle = EmbedProvider.attachOverlay( activity = this@MainActivity, appUserId = userId, visibilityConfig = EMBED_VISIBILITY, navController = navController, ) } NavHost(navController, startDestination = "home") { /* … */ } } ``` Use 13.1 **or** 13.2 for a given Activity, not both. ### 13.3 Existing Compose hierarchy (alternative to `attachOverlay`) [#133-existing-compose-hierarchy-alternative-to-attachoverlay] ```kotlin EmbedProviderComposable( currentScreen = "home", appUserId = userId, visibilityConfig = EMBED_VISIBILITY, navController = navController, ) ``` **VALIDATE:** rotate the mounting Activity: the widget reappears immediately, exactly once. *** ## 14. Screen names: how the SDK identifies screens [#14-screen-names-how-the-sdk-identifies-screens] Visibility config is matched against the **name the SDK reports**. Matching is **exact and case-sensitive**. A config entry no screen ever reports hides the widget **silently**. ### 14.1 The naming ladder (highest wins) [#141-the-naming-ladder-highest-wins] 1. **Host name:** `handle.setCurrentScreen("…")` or `attachOverlay(currentScreen = "…")` 2. **NavController destination:** Compose: the `android:label` if set, else the route **truncated at the first `/`** (`receipt/{orderId}` → `receipt`; the full template also matches with alias matching on). XML nav graphs: `android:label`. **Every XML destination needs `android:label`**, or it reports a numeric id no config can match. 3. **Fragment class name** (top-level resumed Fragment) 4. **Activity class name** Once the host names **one** screen by hand (rung 1), automatic Fragment naming stands down for the session: **name them all**, or a silent screen keeps the previous name. ### 14.2 Matching flags (optional) [#142-matching-flags-optional] ```kotlin EmbedSDK.setScreenMatching(EmbedScreenMatching( aliasMatching = true, // a screen answers to all its names (route template, label, class) ancestorMatching = false, // children inherit a parent's allow; global; leave off unless asked fingerprintScreens = false, // auto-name unannounced sub-screens from content; off when the host names them )) ``` `null` leaves each flag to the dashboard. `EmbedScreenMatching.ALL` turns all three on. ### 14.3 Shapes that need host code [#143-shapes-that-need-host-code] | Shape | Do this | | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Manual `FragmentTransaction`s / Activity-per-screen | Works automatically (rungs 3–4). Name screens with `handle.setCurrentScreen` only if class names are not what the config should use. | | **Compose state tabs** inside one route (`BottomNavigation` switching a `when`) | On every tab switch: `handle.setCurrentScreen(tabName)`. Also re-apply it when returning to that route (key the effect on the current route **and** the tab). | | Several NavHosts / nested graphs | Re-attach with the **active** controller on each tab switch. One controller at a time; a static `currentScreen` on an attach that also passes a controller is ignored. | | Fragments shared across Activities | Composite names (`landing/Details`) via `setCurrentScreen` from the Fragment's `onResume`. | | Two screens with the same name | Give them distinct names, or they merge into one entry. | | Splash / trampoline Activities | Let them be named, then put them in `excludedScreens`. Don't "not attach" there. | | `EmbedSDK.setCurrentScreen(...)` | **Event enrichment only. Never moves the widget.** Use `handle.setCurrentScreen`. | ### 14.4 WebView screens (multi-page web flows) [#144-webview-screens-multi-page-web-flows] The SDK cannot see inside a WebView (the page is one opaque node), and navigation never announces web page changes. To give each web page its own screen on the backend **while the widget follows the route that hosts the WebView**, call `setSubScreen`: ```kotlin webViewClient = object : WebViewClient() { // Fires on full loads AND single-page-app route changes (pushState) override fun doUpdateVisitedHistory(view: WebView, url: String?, isReload: Boolean) { if (!isReload) embedHandle?.setSubScreen(webPageName(url)) } } /** Stable, privacy-safe name: no query string, no IDs, no session hashes. */ fun webPageName(url: String?): String { val segments = url?.let { Uri.parse(it).pathSegments }.orEmpty() .filterNot { it.length > 24 || it.any(Char::isDigit) } return "web/" + segments.joinToString("/").ifEmpty { "home" } } ``` * List the **route / screen that hosts the WebView** in `allowedScreens`, **not** page names. No `EmbedScreenMatching` flag is needed. * Page hops don't re-run the widget's entrance and **don't end a live call**. The next navigation clears the page name automatically. `setSubScreen(null)` returns to the route. * **Never use `setCurrentScreen` for web pages.** It replaces the route, so an unlisted page name hides the widget on a route that allows it. * Never put tokens, session IDs, user IDs or personal data in the name. Ignore a WebView "title" that is just the URL echoed back. * The agent **cannot read or fill web forms**; native chrome around the WebView is captured. **VALIDATE (debug):** each navigation logs `[Screen] A#n → B#n+1 (nav|host|fragment|activity|sub)`. Write down every reported name, then write the config against that list. A hidden screen logs `[Visibility] '' NOT allowed — hidden`. *** ## 15. Visibility, groups & the call licence [#15-visibility-groups--the-call-licence] ```kotlin val EMBED_VISIBILITY = EmbedButtonVisibilityConfig( allowedScreens = listOf("home", "loan_details"), excludedScreens = listOf("login", "mpin", "payment"), showDelay = 0L, groups = listOf( EmbedButtonGroupConfig( id = "kyc_journey", screens = listOf("kyc_start", "kyc_upload", "kyc_review"), continuity = EmbedButtonContinuity.CONTINUOUS, // call survives hops inside delayMs = 0L, delayPolicy = EmbedButtonDelayPolicy.ONCE_PER_GROUP_ENTRY, ) ), defaultInset = EmbedButtonInset(bottom = 96), // dp; clear a bottom bar // endCallWhenHiddenByVisibility = true // default; see below ) ``` Imports: `EmbedButtonVisibilityConfig`, `EmbedButtonGroupConfig`, `EmbedButtonContinuity`, `EmbedButtonDelayPolicy` are in `ai.revrag.embed.android`; `EmbedButtonInset` is in `ai.revrag.embed.android.ui` (fields `right`, `bottom`, `left`, `top`, in dp). Rules: * **Empty `allowedScreens` = everywhere.** A non-empty `groups` list is itself an allowlist (group screens show the widget). `excludedScreens` beats everything. * Hide a screen with `excludedScreens`, **never** by not attaching there: the SDK follows Activities on its own. * **Call licence:** a call starts only where the widget shows, **ends** when the user lands where it doesn't (`endCallWhenHiddenByVisibility = true` by default), ends when the app goes to background (no foreground service), and ends on any hop between allowed screens that are **not** in the same `CONTINUOUS` group. * `EmbedButtonDelayPolicy`: `PER_SCREEN` (default), `ONCE_PER_GROUP_ENTRY`, `ONCE_PER_APP_SESSION`. Explain delays to the developer: a 2500 ms delay looks like "not working" for 2.5 s. * The dashboard's position, if set, wins over `defaultInset` / group `inset`. **VALIDATE:** during a live call, navigate inside a `CONTINUOUS` group: the call persists. Hop to an ungrouped or excluded screen: the call ends (`agent_conversation_ended`). *** ## 16. Dialogs, bottom sheets, other windows [#16-dialogs-bottom-sheets-other-windows] A dialog has its own window above the Activity; elevation/`zIndex` can't cross it. The SDK **moves its single overlay** into the topmost dialog window and back. * Automatic adoption (default) covers `DialogFragment`, `BottomSheetDialogFragment`, plain `Dialog`, Compose `Dialog` and `ModalBottomSheet`. Adoption of non-`DialogFragment` windows uses a hidden API that fails on some OEM builds (e.g. ColorOS 13). * Guaranteed path where adoption can't be trusted: `EmbedProvider.attachOverlay(dialog, requireActivity())` **synchronously from `onStart()`** (no `post`, no teardown). * A dialog that hosts the widget needs a full-screen, transparent window with animations off: ```kotlin dialog.window?.apply { setLayout(MATCH_PARENT, MATCH_PARENT) setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) setDimAmount(0f) setWindowAnimations(0) } ``` On Android 14+, prefer a **transparent `windowBackground` in the dialog theme**. A dialog created opaque and made transparent later leaves visual trails/stuck ripples behind the widget. * A full-screen `DialogFragment` used as a **screen** (MPIN, PAN verify) needs both the dialog attach **and** `handle.setCurrentScreen(...)`. Adoption never renames. **VALIDATE:** open every dialog/sheet while the widget is visible: it rises above it and returns on dismiss, cancel, back, and rotation. *** ## 17. Calls (programmatic API) [#17-calls-programmatic-api] The SDK requests `RECORD_AUDIO` itself on the first call and shows its own explainer after a permanent denial. **Write no permission code.** (Optional early ask: `EmbedSDK.checkPermissions(context) { granted -> }`.) ```kotlin EmbedSDK.expandWidget() // show the in-call UI first EmbedSDK.startCall(activity) { ok -> } // default voice co-pilot EmbedSDK.startCall(activity, AgentTriggerMode.WORKFLOW) { ok -> } // full avatar surface EmbedSDK.endCall() EmbedSDK.isCallActive() // Boolean EmbedSDK.collapseWidget() EmbedSDK.sendText("…") { result -> } // typed message into the call ``` * `AgentTriggerMode`: `CO_PILOT` (agent works on the host's screens) or `WORKFLOW` (RevRag-owned avatar surface). Passed **per call** only. A call started by tapping the widget is always the default co-pilot. To get `WORKFLOW` on every call, the app must start calls itself. * Host-started calls: call `expandWidget()` **before** `startCall()`, on a real user gesture (it fires the tap-to-open analytics event). `startCall` alone leaves a live mic behind a collapsed button. * `startCall`'s result is a plain `Boolean`. Refusal reasons (mic denied, no user id, token failure) go to the backend as an `error` analytics event and, in debug builds, to logcat as `[StartCall] refused — `. * Video is a dashboard setting (`media_mode`), not a client flag. **VALIDATE (physical device, never an emulator):** tap the widget on a fresh install → system mic prompt → call connects (`agent_conversation_started`) → end → second call starts cleanly. Deny twice, tap again → the SDK's own explainer with "Open Settings" appears. *** ## 18. Agent readiness (only if action intelligence is on) [#18-agent-readiness-only-if-action-intelligence-is-on] The agent reads the **accessibility/semantics tree**, the same data TalkBack uses. * Give every actionable element a stable identity: `Modifier.testTag("checkout.pay")` in Compose, `android:id` in XML. Without one, ids derive from text/position and shift. * Icon-only buttons need `contentDescription`. Custom-drawn / Canvas controls need semantics (`contentDescription`, range info, a setter). **Self-test: if TalkBack can read and adjust it, so can the agent.** * Selection by style only (custom chips/tabs/radios) → expose `selected` / `toggleableState`. * One toolkit per screen: in hybrid screens only one substrate is captured (XML chrome inside a Compose screen, or `AndroidView` content inside Compose, is invisible). * Password/secure fields are **redacted at capture and refused at dispatch**. * `FLAG_SECURE` screens **are** captured (screen text leaves the device). Confirm with whoever set the flag. * The UI-graph cache captures every screen landing, **including `excludedScreens`**, while the tenant's cache flags are on. Disclose this to the developer; turning off `actionIntelligence` does not stop it. **VALIDATE:** ask the agent to tap the floating widget itself; it must refuse (the SDK's own UI is never a target). *** ## 19. Logging & diagnostics [#19-logging--diagnostics] | Build | What you see | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Debug build of the host app** | PII-safe lifecycle logs: `adb logcat \| grep RevragEmbed`. Screen changes, visibility decisions, call steps, capture **counts**. Never screen text or payload contents. | | **Release build** | **Nothing** from the SDK. A silent logcat is normal, not a fault. Diagnose through `onResult`, `EmbedSDK.widgetConfig`, and the `ANALYTICS_DATA` listener. | Key debug lines: `[Init] … Stage 1…5`, `[Screen] A#n → B#m (source)`, `[Visibility] '' NOT allowed — hidden`, `[Visibility] '' delay elapsed — showing`, `[EmbedProvider] Route change: 'A' → 'B' — endCall()`, `[StartCall] refused — …`, `[LiveKit] Room connected`, `[UITree] capture: N nodes (…)`. Observe SDK analytics from the host (works in release too): ```kotlin EmbedSDK.on(EventKeys.ANALYTICS_DATA) { data -> Log.d("Embed", "${data["event_name"]}") } EmbedSDK.onAgent(AgentEvent.AGENT_CONNECTED) { /* … */ } ``` Events to expect: `agent_visible`, `agent_conversation_started`, `agent_conversation_ended`, `microphone_permission_allow` / `microphone_permission_denied`. Gate any host-side event logging on `BuildConfig.DEBUG`; payloads can carry user data. *** ## 20. Event ordering validation [#20-event-ordering-validation] Confirm on a healthy first session: 1. `Stage 3 … GET /embedded-agent/initialize` → `Stage 5` → `Initialization complete` 2. `USER_DATA` sent after login 3. `[Screen] … → ` → widget visible → `agent_visible` 4. Tap / `startCall` → `[LiveKit] Room connected` → `agent_conversation_started` 5. End → `agent_conversation_ended` Invalid patterns: a call before `USER_DATA` (`[StartCall] refused — app_user_id is not set …`), `agent_visible` never firing (re-check §8, §10, §14), two widgets on screen (two mounts, §13). *** ## 21. Release build [#21-release-build] * **No ProGuard/R8 rules needed.** The AAR ships consumer rules for its classes, shaded Ktor, and the Compose semantics it relies on. * **Smoke-test the minified release build**, not only debug (R8 has silently broken capture layers before). * If the host ships an **SLF4J binding** (`logback-android`, `slf4j-android`), the SDK's HTTP client logs request URLs at INFO in release, and some carry `app_user_id`. Report it to RevRag; there is no host switch. * Ship an App Bundle (LiveKit/WebRTC native libs are large). * Two library warnings are expected even in release and are not SDK faults: Lottie (`You have N images…`, from the avatar animation) and 3 SLF4J "no binding" lines at startup. *** ## 22. Failure conditions & unknown-unknowns (detect → why → do) [#22-failure-conditions--unknown-unknowns-detect--why--do] | Condition | Expected / what to do | | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Wrong key or key/environment mismatch | No widget, no crash, `widgetConfig` stays null. Fix the pair. | | `RECORD_AUDIO` missing from merged manifest | No mic prompt, every call fails. Add it (§5). | | `app_user_id` never set | `startCall` refused; cache never uploads. §10. | | Widget never appears on a screen | Reported name ≠ config string (§14), or excluded, or delay, or null config (§8). | | Widget vanishes mid-flow | Teardown code (`detach()`), or the screen's name changed (host named one screen but not this one). | | Two widgets | Two mount mechanisms (§13). | | WebView page hides the widget | `setCurrentScreen` used for pages; switch to `setSubScreen` (§14.4). | | Call ends on navigation | Screens not in one `CONTINUOUS` group, or next screen hidden/excluded (§15). | | Call ends on background / Custom Tabs / payment SDK | By design (no foreground service; widget not shown there). Confirm acceptance. | | Incoming phone call during an agent call | Not handled: the agent keeps talking. Disclose. | | Another VoIP/WebRTC SDK in the app | Ask; test both together. No coexistence guarantee. | | Corporate network / VPN | Allowlist on 443: `embed.revrag.ai`, `revrag-dev.s3.ap-south-1.amazonaws.com` (in-call icons), and the LiveKit server + TURN from the token response. If UDP is blocked, TURN over TCP/TLS is needed. | | Multi-window / split screen | Second pane has no widget. Picture-in-picture untested. Set expectations. | | `android:process` | Only the initializing process has an agent. | | Bluetooth headset | Audio routes to it when connected for calls; on disconnect it falls back to the speaker. Verify on device. | | Host overrides the overlay with its own top-level window/portal | Verify z-order on device. | | Shared device, user switch | `clearStorageCache()` on logout (§10). | | 16 KB page-size devices (Android 15) | Not certified for LiveKit 2.27.0 natives. Ask RevRag before certifying. | *** ## 23. Final checklist & report [#23-final-checklist--report] ```text [ ] App shape, toolchain, navigation, Activities, WebViews, dialogs detected [ ] Toolchain at floor: minSdk 24, compileSdk 34, AGP 8.1.1+, Kotlin 2.0+, JDK 17 [ ] JitPack + mavenCentral + google repositories; ai.revrag:embed-android:1.2.0; sync + assembleDebug clean [ ] No LiveKit force-downgrade; no second WebRTC stack (or flagged) [ ] Merged manifest has INTERNET + RECORD_AUDIO; no BLUETOOTH_CONNECT / SYSTEM_ALERT_WINDOW / cleartext [ ] EmbedSDK.initialize in Application.onCreate; onResult logged; widgetConfig non-null on device [ ] Key/environment pair verified; dashboard config + avatar exist for this app version [ ] USER_DATA (app_user_id) after login; clearStorageCache() on logout [ ] Exactly one mount mechanism; no detach/teardown; NavController attach inside setContent (Compose) [ ] Reported screen names listed; config written against them; tabs/multi-NavHost/shared fragments handled [ ] WebView pages via setSubScreen (not setCurrentScreen); route listed in allowedScreens [ ] allowed / excluded / CONTINUOUS groups / delays / insets chosen by the developer [ ] Dialogs and sheets verified on device (incl. rotation) [ ] Call start / connect / end / restart on a physical device; mic deny path; CONTINUOUS hop keeps the call [ ] Agent readiness: testTag / android:id / contentDescription on key controls; secure fields redacted [ ] Minified release build smoke-tested; SDK silent in release logcat [ ] §22 conditions reviewed and disclosed ``` **Final report format** ```text Integration Status: SUCCESS / PARTIAL / FAILED SDK version: | AGP: | Kotlin: | compileSdk: | minSdk: | UI toolkit: | Navigation: | Activities: Dependencies added/changed (+why): Manifest changes: Init: placement + onResult handling + widgetConfig observed: Identity: USER_DATA location | logout clearStorageCache location: Mount: mechanism + where: Screens reported by the SDK: Visibility — allowed: | excluded: | groups (continuity): | delays: | insets: WebView screens + setSubScreen naming: Dialogs/sheets verified: Calls — who starts them | trigger mode | device results: Validation — Init: | Key/env: | App version: | USER_DATA: | Widget config: | Widget rendering: | Screen names: | Call flow: | Dialogs: | Release build: | Logs: Disclosures (background calls, cache on excluded screens, FLAG_SECURE, SLF4J, VPN): Remaining issues (app-side or SDK-side): ``` Do not declare SUCCESS while any critical validation is failing. *** *Companion docs: `docs/INTEGRATION.md` (full mounting + navigation examples), `docs/ANDROID_SDK_API.md` (API reference), `docs/FDE_INTEGRATION_CHECKLIST.md` (field checklist), `docs/ANDROID_1.2.0_INTEGRATION_CHANGES.md` (what's new in 1.2.0).*