Android Chatbot UI Kit

View Release Notes →

dialogs_ChatbotAV_Android is a pre-built, fully managed chat and video-calling UI for Android — unlike the Chatbot SDK, there is no UI to build yourself. It ships three integration levels — drop-in EnxChatClient view, a EnxChatFragment wrapper, and a headless EnxChatSession — plus a Compose-native EnxChat composable. Audio and video calling is powered by the vendored Enx_UIKit_Android and Enx-Rtc-Android native libraries.

Android Chatbot UI Kit v1.0.0  ·  Released July 16, 2026

First release of the Android Chatbot UI Kit. See Release Notes for what's included.

  Download Android Chatbot UI Kit v1.0.0
Setup

Requirements

RequirementValue
minSdkVersion24
compileSdk / targetSdk36
Kotlin2.0.21
Android Gradle Plugin9.0.1
Compose BOM2024.09.00
Java source/target compatibility11

Installation

Option A — Source Module

// settings.gradle.kts
include(":dialogs_ChatbotAV_Android")

// app/build.gradle.kts
dependencies {
    implementation(project(":dialogs_ChatbotAV_Android"))
}

Option B — Standalone AAR

dependencies {
    implementation(files("libs/dialogs_ChatbotAV_Android-1.0.0.aar"))
}

Required Permissions

Add these to your app's AndroidManifest.xml. They also merge in automatically from the SDK and its bundled AARs, but you should still be aware of them since several require runtime consent.

<!-- Always required — plain chat -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- Required only if you enable audio/video call actions (EnxChatConfig(isCallView = true)) -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />

Before starting a call, request the dangerous runtime permissions yourself:

ActivityCompat.requestPermissions(
    this,
    arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO),
    REQUEST_CODE_CALL_PERMISSIONS
)

SYSTEM_ALERT_WINDOW (“display over other apps”) cannot be requested via a runtime dialog — direct users to Settings.ACTION_MANAGE_OVERLAY_PERMISSION if your target devices need the call overlay. On Android 12+, also request BLUETOOTH_CONNECT/BLUETOOTH_SCAN if you want Bluetooth headset routing during calls.

Usage Patterns

Pattern 1 — Jetpack Compose (Quick Start)

@Composable
fun ChatScreen(botId: String, host: String) {
    val config = remember { EnxChatConfig(botId = botId, host = host) }

    AndroidView(
        factory = { context ->
            val activity = context as ComponentActivity
            EnxChatClient(context).apply {
                bindToLifecycle(owner = activity)
                start(config, object : EnxChatBotListener() {
                    override fun onBotConnected(botConnectedInfo: BotInfo) { /* update your header, etc. */ }
                    override fun onClose() { /* pop the screen */ }
                })
            }
        }
    )
}

Pattern 2 — Compose with a Custom App-Owned Header

var botName by remember { mutableStateOf("Assistant") }
var enxChatClient by remember { mutableStateOf<EnxChatClient?>(null) }

Column {
    Row {
        Text(botName)
        EnxCallView(iconTint = Color(0xFF111B21))          // shows call icons only if isCallView = true
        IconButton(onClick = { enxChatClient?.resetConversation() }) { /* refresh icon */ }
    }
    AndroidView(
        factory = { context ->
            EnxChatClient(context).apply {
                bindToLifecycle(this@MyActivity)
                start(
                    EnxChatConfig(botId = botId, host = host, isCallView = true),
                    object : EnxChatBotListener() {
                        override fun onBotConnected(botInfo: BotInfo) {
                            botName = botInfo.name.ifBlank { "Assistant" }
                        }
                        override fun onClose() { finish() }
                    }
                )
                enxChatClient = this
            }
        }
    )
}

Pattern 3 — Kotlin (Plain View / Activity)

class ChatActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val config = EnxChatConfig(botId = "YOUR_BOT_ID", host = "https://your-host.example.com")

        val chatClient = EnxChatClient(this)
        setContentView(chatClient)
        chatClient.bindToLifecycle(owner = this)
        chatClient.start(config, object : EnxChatBotListener() {
            override fun onBotConnected(botConnectedInfo: BotInfo) {}
            override fun onClose() { finish() }
        })
    }
}

Pattern 4 — Java

public class ChatActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        EnxChatClient chatClient = new EnxChatClient(this);
        setContentView(chatClient);

        EnxChatConfig config = new EnxChatConfig("YOUR_BOT_ID", "https://your-host.example.com");

        chatClient.bindToLifecycle(this);
        chatClient.start(config, new EnxChatBotListener() {
            @Override
            public void onBotConnected(BotInfo botConnectedInfo) {}

            @Override
            public void onClose() { finish(); }
        });
    }
}

Pattern 5 — Fragment-First Apps

val fragment = EnxChatFragment.newInstance(botId = "YOUR_BOT_ID", host = "https://your-host.example.com")
supportFragmentManager.beginTransaction()
    .replace(R.id.container, fragment)
    .commit()

// After the fragment's view is created:
fragment.setUiListener(object : EnxChatBotListener() {
    override fun onBotConnected(botConnectedInfo: BotInfo) {}
    override fun onClose() {}
})
fragment.bindToHostLifecycle()
fragment.start()

Listening to Richer Events

For anything beyond “connected”/“closed”, use setSessionListener(...) in addition to (or instead of) setUiListener(...):

chatClient.setSessionListener(object : EnxChatSessionListenerAdapter() {
    override fun onBotMessage(chatMessage: ChatMessage) { /* render/log */ }
    override fun onTyping(isTyping: Boolean) { /* show typing indicator */ }
    override fun onError(error: String) { /* surface to user / log */ }
    override fun onVideoCallRequested(iFrameUrl: String) { /* SDK auto-launches the call screen; observe only if needed */ }
    override fun onNativeUIKitCallRequested(roomToken: String) { /* same — native call path */ }
})

Both call-request callbacks are informational — EnxChatClient/EnxChat already launch EnxVideoCallActivity automatically when the bot requests a call. You do not need to start it yourself.

Sending Messages, Attachments, and Files

// Plain text (via EnxChatSession, if using the headless API)
session.sendMessage("Hello!")

// With a file attachment (image/document/video picked via a system picker Uri)
session.sendMessage(
    text = "Here's my document",
    attachments = listOf(ChatAttachment(uri = pickedUri, type = AttachmentType.DOCUMENT))
)

EnxChatClient's built-in chat UI (EnxChatScreen) already wires up the attachment picker (camera, gallery, document, emoji) for you — most apps never need to call sendMessage/sendFile directly unless they're building a fully custom UI on top of EnxChatSession/EnxChat.

Resetting a Conversation

chatClient.resetConversation()       // EnxChatClient
fragment.resetConversation()         // EnxChatFragment
session.resetConversation()          // EnxChatSession
controller.resetConversation()       // EnxChatController, from EnxChat composable

Tearing Down Correctly

Always prefer bindToLifecycle(owner) so disconnect()/release() happen automatically. If managing lifecycle manually:

override fun onDestroy() {
    chatClient.disconnect()
    chatClient.release()
    super.onDestroy()
}

EnxChatClient also calls release() automatically in onDetachedFromWindow() as a safety net against leaked WebViews.

API Reference

EnxChatConfig data class · com.dialogs_chatbotav_android.core

data class EnxChatConfig @JvmOverloads constructor(
    val botId: String,
    val host: String,
    val path: String = "",
    val isCallView: Boolean = false,
    val enableHistory: Boolean = false
)
FieldRequiredDescription
botIdYesEnableX chatbot identifier.
hostYesBase API host. Must start with http:// or https:// or the SDK reports ENX_CONFIG_INVALID: host must start with http:// or https://.
pathNoReserved/advanced routing path.
isCallViewNoEnables the built-in audio/video call action icons in the chat UI header (see EnxCallView).
enableHistoryNoEnables conversation history restoration.

Validation errors reported via the active listener's onError(String): ENX_CONFIG_INVALID: botId is required, ENX_CONFIG_INVALID: host is required, ENX_CONFIG_INVALID: host must start with http:// or https://.

EnxChatState enum

CREATED · INITIALIZING · INITIALIZED · CONNECTING · CONNECTED · DISCONNECTED · ERROR · RELEASED

EnxChatSessionListener interface — all methods have default no-op bodies

interface EnxChatSessionListener {
    fun onStateChanged(state: EnxChatState) {}
    fun onChatReady() {}
    fun onConnected(sessionId: String) {}
    fun onConversationReady(conversationId: String) {}
    fun onMessageSent(message: String) {}
    fun onBotResponse(message: String) {}
    fun onBotMessage(chatMessage: ChatMessage) {}
    fun onTyping(isTyping: Boolean) {}
    fun onDisconnected() {}
    fun onBotInfo(botInfo: BotInfo) {}
    fun onClear(dataJson: String) {}
    fun onConversationReset(dataJson: String) {}
    fun onAudioCallClick() {}
    fun onVideoCallClick() {}
    fun onError(error: String) {}
    fun onVideoCallRequested(iFrameUrl: String) {}
    fun onNativeUIKitCallRequested(roomToken: String) {}
    fun onVideoCallEnded() {}
}

Use EnxChatSessionListenerAdapter (an open class implementing this interface with empty overrides) from Java to override only what you need.

EnxChatSession Level 3 — headless API

class EnxChatSession {
    companion object {
        @JvmStatic fun create(activity: Activity, config: EnxChatConfig): EnxChatSession
        @JvmStatic @JvmOverloads fun create(
            activity: Activity, botId: String, host: String,
            path: String = "", isCallView: Boolean = false, enableHistory: Boolean = false
        ): EnxChatSession
    }

    val state: StateFlow<EnxChatState>

    fun setSessionListener(listener: EnxChatSessionListener?)
    fun addSessionListener(listener: EnxChatSessionListener)
    fun removeSessionListener(listener: EnxChatSessionListener)

    fun initialize()
    fun connect()
    @JvmOverloads fun initializeAndConnect(forceConnectState: Boolean = true)
    fun start()  // = initializeAndConnect(true)

    @JvmOverloads fun sendMessage(text: String, attachments: List<ChatAttachment> = emptyList())
    fun disconnect()
    fun resetConversation()
    fun release()

    fun getWebView(): WebView
    fun saveState(): Bundle?
    fun restoreState(savedState: Bundle?)
    fun getCurrentState(): EnxChatState
}

Use this only for headless lifecycle control or custom UI orchestration; standard apps should use EnxChatClient.

EnxChatClient extends android.widget.FrameLayout — Level 1 & 2, recommended API

The default, recommended integration point — usable directly as a Kotlin/Java View, or wrapped in Jetpack Compose via AndroidView.

class EnxChatClient @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {

    fun setConfig(config: EnxChatConfig)

    @JvmOverloads fun start(config: EnxChatConfig, listener: EnxChatBotListener? = null)

    @JvmOverloads fun startWithBot(
        botId: String, host: String, path: String = "",
        isCallView: Boolean = false, enableHistory: Boolean = false,
        listener: EnxChatBotListener? = null
    )

    @JvmOverloads fun bindToLifecycle(
        owner: LifecycleOwner, disconnectOnStop: Boolean = false, releaseOnDestroy: Boolean = true
    )
    fun unbindFromLifecycle()

    fun initialize()
    fun connect()
    fun disconnect()
    fun release()
    fun getState(): EnxChatState

    fun setProfilePictureUrl(url: String?)
    fun setHeaderColor(@ColorInt colorInt: Int)
    fun setUiListener(listener: EnxChatBotListener?)
    fun setSessionListener(listener: EnxChatSessionListener?)
    fun resetConversation()

    @Deprecated("Use startWithBot(...) or start(EnxChatConfig, listener)")
    @JvmOverloads fun setBotIdAndHost(
        botId: String, host: String, path: String = "",
        isCallView: Boolean = false, enableHistory: Boolean = false
    )
}

Standard lifecycle (ready-made mode): start(...)disconnect()release()
Lifecycle-safe mode (recommended): bindToLifecycle(owner) + start(...) — disconnect/release handled automatically on onStop/onDestroy
Explicit lifecycle (advanced): initialize()connect()disconnect()release()

EnxChatClient automatically persists/restores WebView state across configuration changes (e.g. screen rotation) via onSaveInstanceState/onRestoreInstanceState, and calls release() automatically in onDetachedFromWindow() as a leak-safety net.

EnxChatBotListener abstract class — simplified, user-facing callbacks

abstract class EnxChatBotListener {
    open fun onBotConnected(botConnectedInfo: BotInfo) {}
    open fun onClose() {}
}

This is intentionally the smallest listener surface for apps that just want “chat connected” + “user closed chat” signals. For richer events (messages, typing, errors, call requests), use EnxChatSessionListener via setSessionListener(...).

EnxChatFragment extends androidx.fragment.app.Fragment — Level 2

class EnxChatFragment : Fragment() {
    companion object {
        @JvmStatic @JvmOverloads fun newInstance(
            botId: String, host: String, path: String = "",
            isCallView: Boolean = false, enableHistory: Boolean = false
        ): EnxChatFragment
    }

    fun setUiListener(listener: EnxChatBotListener?)
    fun setSessionListener(listener: EnxChatSessionListener?)
    @JvmOverloads fun bindToHostLifecycle(disconnectOnStop: Boolean = false, releaseOnDestroy: Boolean = true)
    @JvmOverloads fun bindToLifecycle(owner: LifecycleOwner, disconnectOnStop: Boolean = false, releaseOnDestroy: Boolean = true)

    fun start()
    fun initialize()
    fun connect()
    fun disconnect()
    fun release()
    fun resetConversation()
    fun getState(): EnxChatState?
}

Use only when your host architecture is fragment-first; internally it just creates and delegates to an EnxChatClient.

Compose-Native API com.dialogs_chatbotav_android.ui.compose

EnxChat (composable)

@Composable
fun EnxChat(
    enxChatConfig: EnxChatConfig,
    session: EnxChatSession? = null,
    sessionListener: EnxChatSessionListener? = null,
    controller: EnxChatController? = null,
    profilePictureUrl: String? = null,
    headerColor: Color = Color.White,
    onSdkReady: (() -> Unit)? = null,
    onBotInfo: ((BotInfo) -> Unit)? = null,
    onAudioCallClick: (() -> Unit)? = null,
    onVideoCallClick: (() -> Unit)? = null,
    onMessageSent: ((String, List<ChatAttachment>) -> Unit)? = null,
    onBotResponse: ((String, List<ChatAttachment>) -> Unit)? = null,
    onClose: (() -> Unit)? = null,
    modifier: Modifier = Modifier
)

If session is omitted, EnxChat creates and owns its own EnxChatSession internally (recreated automatically if enxChatConfig changes, released on composable disposal). Prefer EnxChatClient via AndroidView unless you specifically need Compose-native customization.

EnxChatController

class EnxChatController {
    fun resetConversation()
}

Pass into EnxChat(controller = ...) to trigger a conversation reset imperatively from outside the composable.

EnxCallView (composable)

@Composable
fun EnxCallView(
    onAudioCallClick: (() -> Unit)? = null,
    onVideoCallClick: (() -> Unit)? = null,
    iconTint: Color = Color.Black,
    modifier: Modifier = Modifier
)

Renders audio/video call icon buttons for a custom app-owned header; renders nothing unless EnxChatConfig.isCallView = true was used for the active session. If click handlers are omitted, falls back to the handlers registered by the active EnxChat/EnxChatClient instance.

Models

com.dialogs_chatbotav_android.model.chat

enum class AttachmentType { IMAGE, VIDEO, DOCUMENT, EMOJI }

data class ChatAttachment(
    val uri: Uri? = null,
    val url: String? = null,
    val type: AttachmentType,
    val fileName: String? = null,
    val fileSize: Long? = null,
    val mimeType: String? = null
)

data class QuickReply(
    val title: String,
    val payload: String? = null,
    val type: String? = null   // "postback" | "url" | "phone_number" | "calendar" ...
)

enum class MessageType {
    TEXT, QUICK_REPLY, BUTTONS, IMAGE, VIDEO, AUDIO, FILE, CARD, CAROUSEL, SYSTEM, UNKNOWN
}

data class ChatMessage(
    val id: String = UUID.randomUUID().toString(),
    val text: String = "",
    val isFromUser: Boolean,
    val timestamp: Long = System.currentTimeMillis(),
    val attachments: List<ChatAttachment> = emptyList(),
    val messageType: MessageType = MessageType.TEXT,
    val quickReplies: List<QuickReply> = emptyList(),
    val buttons: List<QuickReply> = emptyList(),
    val allowMultiple: Boolean = false,
    val rawData: Map<String, Any>? = null
)

ChatAttachment.type and ChatMessage.isFromUser have no default value — when constructing these from Java or by position, supply them explicitly.

com.dialogs_chatbotav_android.model.botinfo

data class BotInfo(
    val showBotInfoPage: Boolean = false,
    val name: String = "",
    val description: String = "",
    val details: BotDetails = BotDetails(),
    val startupMessage: String = "",
    val selfTriggeredBot: Boolean = false,
    val embeddedMediaIframe: String? = null,
    val clickToCall: String = "text",     // "text" = full chat UI, else minimal click-to-call UI
    val agentHuntingStatusMessage: AgentHuntingStatusMessage? = null,
    val chatTheme: ChatTheme = ChatTheme(),
    val startupForm: StartupForm = StartupForm(),
    val languages: List<String> = emptyList(),
    val disableNotificationSound: Boolean = false,
    val security: Security = Security(),
    val lazySocket: Boolean = false,
    val maxMessageLength: Int = 1000,
    val alwaysScrollDownOnMessages: Boolean = false
)

BotInfo.clickToCall == "text" (default) renders the full chat UI (EnxChatScreen); any other value renders the minimal click-to-call UI (EnxClickToCallMinimalScreen) — a single “Start call” button plus status line, themed from chatTheme.theme.

Nested types: BotDetails (contact/legal URLs, avatar/cover images), AgentHuntingStatusMessage (status copy while waiting for a live agent), ChatTheme/UserTheme/BotTheme/WidgetTheme (colors, bubble style, widget position), StartupForm (pre-chat form field toggles), Security (escapeHTML).

BotInfoParser (internal object) — fun parse(jsonString: String): BotInfo? — parses the raw JSON delivered by Android.onBotInfo(...); returns null on malformed JSON (caught internally, logged, onError is not raised for this specific case).

Error / Event Reference

EventSourceMeaning
onError("ENX_CONFIG_INVALID: ...")EnxChatClient/EnxChatSessionConfig validation failed before session start.
onError("WebView error: <description>")EnxChatBot (WebViewClient.onReceivedError)Main-frame WebView load failure.
onError("Failed to send file: <message>")EnxChatBot.sendFileFile read/base64/JS-bridge failure while sending an attachment.
onError("Conversation reset function is not available in web client")EnxChatBot.resetConversationLoaded web client version doesn't expose any known reset function.
onDisconnected()EnxChatBot.disconnectAlways fires after a disconnect() call, regardless of whether the web client exposed a disconnect function.
Feature Support

Feature Support (v1.0.0)

FeatureSupportedNotes
Three integration levelsYesEnxChatClient (View), EnxChatFragment (Fragment), EnxChatSession (headless), plus a Compose-native EnxChat composable.
Full chat screen (EnxChatScreen)YesDate separators, typing indicator, WhatsApp-style attachment sheet (camera/gallery/document/emoji), quick-reply single- and multi-choice selection, calendar/date-picker quick replies.
Rich in-message attachment previewsYesImage/video fullscreen viewers; document viewer supporting PDF (paginated), JSON (pretty-printed + syntax highlighting), CSV (table view), HTML, Markdown, and plain/code text, with an “open externally” fallback for unsupported types.
Markdown renderingYesVia Markwon (tables + strikethrough extensions).
Message de-duplicationYesJS bridge safeguards (processedBotMessageKeys/processedHistoryEntryKeys) avoid duplicate bubbles from history replay.
Conversation reset detectionYesFree-text bot copy (e.g. “reset conversation”) in addition to explicit quick-reply/button reset actions.
Response timeout fallbackYesA 30-second timeout injects a friendly “having trouble responding” system message if the bot doesn't answer in time.
Message queueing while disconnectedYesMessages sent before CONNECTED are queued and flushed automatically on connect.
Two independent call pathsYesBrowser/iFrame-based calls (onVideoCallRequested, rendered in a dedicated WebView) and native calls via the EnableX UIKit Android SDK (onNativeUIKitCallRequested, rendered using EnxVideoView/EnxSetting/EnxAudioViewConfig) — both launched via the internal EnxVideoCallActivity.
Picture-in-Picture during native callsYes9:16 aspect ratio, triggered on back-press or onUserLeaveHint.
Automatic return-to-chat navigationYescloseAndReturnToChat() brings the host chat Activity back to the foreground when a call ends.
File & attachment sendingYesAccepts Uri, File, ByteArray, or a raw base64 String; auto-detects file name/MIME type; base64-encodes off the main thread.
Config-change survivalYesonSaveInstanceState/onRestoreInstanceState persist WebView state across rotation.
Audio-only native callsNoEnxAudioViewConfig is wired but not currently reachable — isAudioOnly is hardcoded false.
Automatic JS-bridge re-registrationNoEnxChatBot.setEnxChatListener(...) does not re-register the JS bridge if called after initialization — set your listener before start()/initialize().
Reference

Architecture Notes

The UI Kit vendors three native AARs directly under libs/ (not resolved via Maven): Enx_UIKit_Android-2.2.3.aar, Enx-Rtc-Android-release.aar, and libwebrtc.aar. Practical consequences for anyone maintaining an integration against a newer UI Kit build:

  • No automatic version resolution — upgrading the UI Kit means manually downloading/copying a new .aar and bumping the referenced filename.
  • No transitive dependency conflict detection the way Gradle gives you for a mavenCentral()-hosted artifact — a future UI Kit version bumping its own dependency minimums (e.g. ExoPlayer/Gson) must be discovered and reconciled manually.
  • A UI Kit upgrade can silently add new required permissions or components — re-verify the merged manifest after any AAR swap.

Threading Model

  • All @JavascriptInterface callback methods on the internal JS bridge are invoked by the WebView on a background thread (per Android's WebView JS-bridge contract) — they parse JSON synchronously, then call into your listener. Anything that touches Compose/View state downstream must marshal back to the main thread itself (Compose's mutableStateOf/StateFlow collectors handle this automatically; raw listener implementations that touch Views directly must runOnUiThread/post themselves).
  • File sending explicitly launches on Dispatchers.IO for file reading/base64 encoding, then switches to Dispatchers.Main before calling into the WebView (required — WebView APIs are main-thread-only).
  • EnxChatSession's listener fan-out takes a synchronized snapshot of the listener set before iterating, and wraps each callback in a try/catch so one misbehaving listener can't break delivery to the others or crash the bridge thread.

Known Limitations

  • Chat WebView URL is hardcoded to a fixed EnableX-hosted URL; the bundled offline assets are currently unreferenced.
  • SSL error handling differs between the chat WebView (blocks on error) and the call WebView (bypasses).
  • EnxChatBot.setEnxChatListener(...) doesn't re-register the JS interface if called after initialize() — only the first-set listener chain reliably receives callbacks post-init.
  • Audio-only native call UI (EnxAudioViewConfig) is currently dead code — isAudioOnly is hardcoded false.

Common Configuration Errors

Error (delivered via onError)CauseFix
ENX_CONFIG_INVALID: botId is requiredEmpty botIdProvide a non-blank botId.
ENX_CONFIG_INVALID: host is requiredEmpty hostProvide a non-blank host.
ENX_CONFIG_INVALID: host must start with http:// or https://Missing schemePrefix host with https:// (recommended) or http://.
WebView error: <description>Network/WebView load failureCheck device connectivity; the chat client itself is loaded from a fixed EnableX-hosted URL, unrelated to your host value.
Call button does nothingisCallView not set, or CAMERA/RECORD_AUDIO not grantedSet EnxChatConfig(isCallView = true) and confirm runtime permissions were granted before the bot requests a call.