# Android

Native Kotlin chat SDK with a full Jetpack Compose UI: AI chatbot, live-agent handoff, attachments, typing indicators and unread counts.

## Install

`mavenCentral()` must be present in `dependencyResolutionManagement`; consumer R8/ProGuard rules ship in the AAR. minSdk 24, JDK 17.

```kotlin
implementation("cloud.newinstance:liveandaichat:0.5.0")
```

## Quick start

Your backend mints a customer token (see [Session tokens (REST API)](/session-tokens-rest-api)) and hands it to the app:

```kotlin
val chat = LiveAndAiChat.Builder(context)
    .config(LiveAndAiChatConfig(customerToken = tokenFromYourBackend))
    .build()

chat.openChat()
```

`openChat()` starts the bundled chat activity internally; Compose hosts can embed `ChatScreen(sdk, onClose, onPickFile = {}, modifier)` directly.

## Auth modes

Configure exactly one credential. Both, neither, or a bare key ID make `LiveAndAiChatConfig` throw `LiveAndAiChatError` with code `E_CONFIG`. Its `toString()` redacts the secret and the token.

**Customer token mode (recommended).** `LiveAndAiChatConfig(customerToken = ...)`. The SDK sends `Authorization: Bearer <token>` and the server derives the customer from the signed claims, so `setUser` fields are not sent. When a fresh token is due, call `chat.setUserToken(freshTokenFromYourBackend)`: it swaps the credential for every later request and reconnects the realtime subscription without destroying the SDK. A token for the same customer keeps the conversation; the SDK compares the token's subject, not the token string.

**Key mode (trusted internal apps only).** This ships your secret inside the APK, so never use it in an app distributed to the public.

```kotlin
val chat = LiveAndAiChat.Builder(context)
    .config(LiveAndAiChatConfig(apiKey = "sk_live_abc123:YOUR_SECRET_KEY"))
    .build()
chat.setUser(ChatUser(customerName = "Ada", customerEmail = "ada@example.com", customerId = "usr_123"))
```

The SDK sends the full key in `x-api-key`. `ChatUser` requires a non-blank `customerName` (it throws otherwise); switching to a different customer in `setUser` clears the saved conversation and starts fresh. `setUserToken` throws `E_CONFIG` in key mode.

## Errors

`LiveAndAiChat.state` exposes a `ChatSdkError` with a stable `code`, and listeners receive `onError(LiveAndAiChatError)` carrying the same code. Codes are shared with every other chat SDK; see **Authentication and identity -> Diagnosing problems**. An auth failure is reported as `INVALID_IDENTITY_TOKEN` in customer token mode and `INVALID_PUBLIC_KEY` in key mode, which is the difference between minting a fresh token and fixing the key. After an auth failure the SDK does not reconnect with the same credential; a later `setUserToken` restarts it.

## Configuration

| Field | Default | Notes |
|-------|---------|-------|
| `customerToken` | none | Customer token minted by your backend. Selects customer token mode (recommended) |
| `apiKey` | none | The full `keyId:secret`, sent whole. Selects key mode, trusted internal apps only. A bare key ID is rejected with `E_CONFIG` |
| `baseUrl` | `https://service.newinstance.cloud` | Release builds throw on any override (debug-only escape hatch) |
| `gqlPath` / `ssePath` / `wsPath` | `/service` / `/graphql/stream` / `/graphql/ws` | Transport paths |
| `transport` | inherited | `sse` or `ws` override |
| `initialMessage` | none | Message sent automatically when the conversation opens |
| `theme` | dashboard theme | `ChatThemeOverride(...)` local theme, ranked below the dashboard theme and above the built-in one. See [Theming](/theming) |
| `allowRemoteAttachmentUrls` | false | Opt-in: lets `attach()` download and upload http(s) sources |

## API surface

`initialize`, `openChat`, `closeChat`, `sendMessage`, `retryMessage`, `attachFile(bytes, name, mimeType, previewUri)`, `attach(AttachmentRequest, previewUri?)`, `removeAttachment` (cancels an in-flight upload), `clearAttachments`, `updateDraft`, `requestHandoff(reason)`, `connectToAgent(reason)`, `sendTypingStart` / `sendTypingStop`, `setUser`, `setUserToken`, `addListener` / `removeListener`, `destroy`.

Observe state via StateFlows: `lifecycle`, `connectionState`, `messages`, `conversation`, `assignment`, `agentTyping`, `unreadCount`, `orgConfig`, `agentsOnline`, `attachments`, `flowState`, `widgetOpen`, `draftText`; or a listener with `onMessageReceived`, `onMessageSent`, `onTyping`, `onConnectionChange`, `onUnreadCountChange`, `onError`, `onAttachmentUpdate`, `onChatClosed`. Errors carry `type` (NETWORK, VALIDATION, AUTH, SYSTEM) and `recoverable`.

## Programmatic attachments

`attach(AttachmentRequest(source, name, mimeType?, declaredSize?, metadata))` accepts any source: `AttachmentSource.Base64Data`, `DataUri`, `FilePath`, `ContentUri`, `Bytes`, or `RemoteUrl` (config-gated). `AttachmentSource.detect(value)` safely classifies strings. Validation runs before anything is queued: base64 must decode, URIs must be readable, files must be non-empty and at most 25 MB, MIME must be png/jpeg/webp/gif/pdf, a wrong `declaredSize` is rejected as corruption, and the extension must agree with the MIME type. Reads and uploads run off the main thread with real byte progress on the `attachments` flow and `onAttachmentUpdate` (statuses QUEUED, UPLOADING, UPLOADED, FAILED, CANCELLED). A message is never sent with an attachment that has not finished uploading, and attachment contents are never logged.

## The chat close event

`onChatClosed` fires exactly once per chat presentation, whichever way the screen goes away: the in-chat X (`close_button`), the system back button or gesture (`back_navigation`), `closeChat()` (`programmatic`; it now also finishes the chat activity), `destroy()` (`session_ended`), or host navigation (`host_navigation`). The event carries reason, initiator (user/host/sdk), timestamp, conversationId, assignmentId, channel, previousState, unreadCount, hasDraft, pendingAttachmentCount and SDK metadata; never message contents, credentials or attachment data.

## Platform notes

- The SDK manifest already declares INTERNET, ACCESS_NETWORK_STATE and legacy WRITE_EXTERNAL_STORAGE (maxSdkVersion 28) and merges the chat activity; add READ_MEDIA_IMAGES only if your own picker needs it.
- Theming comes from the org configuration (the dashboard can force dark or light; otherwise the system theme applies); there is no local theme parameter.
- All UI strings are Android string resources, so hosts can localise with `values-xx` overrides.
- No push notifications: unread is in-app only via `unreadCount`.
- `destroy()` cancels the SDK scope; any later call throws IllegalStateException, so build a new instance to start again.
- Steps from your connected MCP servers (forms and secure pages) require 0.5.0; see [MCP servers](/mcp-servers).
