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

# Voice calling (Android SDK)

> Add live two-way WebRTC voice calls to your Android app with ai.poly:voice.

`ai.poly:voice` places live, two-way WebRTC voice calls to a PolyAI agent — the companion artifact to the [Android SDK's](/messaging-channel/android-sdk) chat product. It ships **separately** so chat-only apps stay lean (the call path pulls in the native libwebrtc audio engine), and it reuses the messaging `Configuration` plus the same `CallState` / `PolyError.Voice` / `Environment` vocabulary — no new concepts.

Calls are user-initiated: the user taps to call your agent. Inbound (push-triggered) calls are not supported.

<Card title="Source on GitHub" icon="github" href="https://github.com/polyai/android-sdk/tree/main/polyvoice">
  polyai/android-sdk — includes the full polyvoice technical guide and runnable voice example apps.
</Card>

## Installation

```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
// build.gradle.kts
dependencies {
    implementation("ai.poly:messaging:0.9.0")
    implementation("ai.poly:voice:0.9.0")
}
```

## Quickstart

A call needs the **`RECORD_AUDIO`** runtime permission. The SDK declares it in its manifest, but your app must request the grant from the user before starting a call.

```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
import ai.poly.voice.PolyVoice

val call = PolyVoice.call(
    context,
    Configuration(apiKey = "YOUR_API_KEY"),            // connector token — Agent Studio › Connector Settings
    VoiceOptions(webrtcToken = "YOUR_WEBRTC_TOKEN"),   // WebRTC token — same place (see Credentials below)
)

// Observe the call lifecycle (Idle → Connecting → Connected → Ended / Failed).
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        call.state.collect { state ->
            when (state) {
                is CallState.Connected -> showInCallUi()
                is CallState.Failed -> showError(state.error)   // a PolyError.Voice
                is CallState.Ended -> dismissInCallUi()
                else -> Unit
            }
        }
    }
}

// After RECORD_AUDIO is granted:
lifecycleScope.launch { call.start() }   // suspends until the call is connecting; throws on setup failure

// In-call controls:
call.setMuted(true)   // mute the mic
call.end()            // hang up and release the mic
```

`CallState`, `PolyError.Voice.*`, `Configuration`, and `Environment` are the same types from `ai.poly:messaging`. Java callers get `Executor` + `Callback<Unit>` overloads of `start` / `end` / `setMuted`, mirroring the chat API.

## Credentials

A voice call needs **two credentials**, both on your agent in [Agent Studio](https://studio.poly.ai) › Connector Settings (the same connector you use for chat):

| Value                                         | What it is                                                                                  | Required? | Sent as                                   |
| --------------------------------------------- | ------------------------------------------------------------------------------------------- | --------- | ----------------------------------------- |
| **API key** — `Configuration.apiKey`          | your **connector token**                                                                    | **Yes**   | `X-Token` (authenticates the call)        |
| **WebRTC token** — `VoiceOptions.webrtcToken` | the **gateway auth token** for the media connection — a **distinct** token from the API key | **Yes**   | the offer `authToken` + ICE-servers fetch |

Both are always required and always distinct: the API key authenticates the *connector* and is shared by chat and voice, so it lives on the shared `Configuration`; the WebRTC token authenticates the *media gateway* only, so it lives on `VoiceOptions`.

Two more values have sensible defaults, so most apps don't set them:

* **`environment`** defaults to `Environment.US`. Set `.UK` / `.EUW` — or `.cluster("…")` for a named cluster — only if your agent lives in another region. The same `Environment` you use for chat also selects the voice gateway.
* **`hostIdentifier`** (sent as `X-Host`) defaults to your app's package name (`applicationId`).

<Note>
  **Custom / self-hosted gateway:** the WebRTC gateway host is derived from your `Environment`. If you run a dev or self-hosted gateway, set `VoiceOptions.signalingHost` (no scheme, e.g. `"webrtc-gateway.example.com"`) — it's **required** with `Environment.Custom`, since the gateway host can't be derived from a custom messaging endpoint.
</Note>

## Permissions

The SDK's manifest **auto-merges** the three permissions every call needs — you don't declare these:

```xml theme={"theme":{"light":"github-light","dark":"github-dark"}}
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />   <!-- runtime grant required -->
```

`RECORD_AUDIO` is a **runtime** permission — request it before `start()` (the call fails fast with `PolyError.Voice.MediaFailed` if it's missing). Add the rest **only for the optional features you use**:

```xml theme={"theme":{"light":"github-light","dark":"github-dark"}}
<!-- Bluetooth audio output — so BT headsets show up in call.audio (see "Audio output") -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- Keep a call alive while the app is backgrounded (see "Background calls") -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />   <!-- ongoing-call notification -->
```

## Audio output

By default the call **follows the connected accessory** — a wired or Bluetooth headset is used automatically (and auto-switches when you plug/unplug one mid-call); when nothing's connected it falls back to the **loudspeaker** (`VoiceOptions(speakerphone = false)` falls back to the earpiece instead). To let users **pin a specific output**, observe `call.audio` and call `setAudioDevice` — and pass `null` to return to automatic:

```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
// A consistent snapshot: the outputs available now + the active one.
lifecycleScope.launch {
    call.audio.collect { state ->
        renderPicker(state.availableDevices, selected = state.selectedDevice)
    }
}

call.setAudioDevice(speakerDevice)  // route to a device from availableDevices
call.setAudioDevice(null)           // revert to automatic routing (wired > Bluetooth > earpiece/speaker)
```

* `AudioDevice.type` is one of `EARPIECE` / `SPEAKER_PHONE` / `WIRED_HEADSET` / `BLUETOOTH`; `name` is a picker-friendly label. The list updates live as headsets connect/disconnect.
* **Switching is asynchronous** — Bluetooth can take a few seconds to engage. Drive your UI off `call.audio`, not off `setAudioDevice` returning. Selecting an unavailable device is a no-op.
* **Bluetooth needs `BLUETOOTH_CONNECT`.** The library does **not** declare this runtime permission for you; add it to your app and request the grant if you want Bluetooth outputs to appear. Without it, Bluetooth is simply absent — no crash.

## Interruptions (incoming calls, other apps)

The SDK manages audio focus for you — it takes focus on `start()` and releases it on `end()`/teardown. It also **reacts to losing focus** while a call is live, so you don't have to:

* A **transient** loss (a notification, a navigation prompt) **mutes the mic** for the duration and restores it automatically when focus returns — the call stays `Connected`. Nothing to handle.
* A **permanent** loss (the user answers an **incoming phone call**, or another app starts an exclusive audio session) **ends the call**: it surfaces as `CallState.Failed(PolyError.Voice.Interrupted)` and the mic is released.

So all you do is observe `state` and tell the user — the mic is already released for you:

```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
lifecycleScope.launch {
    call.state.collect { state ->
        if (state is CallState.Failed && state.error is PolyError.Voice.Interrupted) {
            showBanner("Call interrupted — tap to call again") // e.g. an incoming phone call ended it
        }
    }
}
```

## Background calls

A `VoiceCall` is a plain object on its own coroutine scope — it is **not** tied to your Activity/Fragment lifecycle, so the SDK won't end a call just because your UI is backgrounded. **However**, when your app goes to the background Android 9+ **cuts mic capture** and **throttles the WebRTC media/network threads**, so the connection silently dies within \~15s and the SDK reports `CallState.Failed(PolyError.Voice.Disconnected)`. To keep a call alive in the background you need **two things** while the call is active:

1. A **microphone foreground service** — grants background mic + keeps the process foregrounded.
2. A **partial wake lock** — keeps the CPU running for the media/network threads.

```xml theme={"theme":{"light":"github-light","dark":"github-dark"}}
<service android:name=".CallForegroundService" android:foregroundServiceType="microphone" android:exported="false" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />               <!-- API 28+ -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />    <!-- API 34+ -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
```

```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
// in the service's onStartCommand, after startForeground(...):
wakeLock = getSystemService(PowerManager::class.java)
    .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "yourapp:voice-call").apply { acquire() }
// release it in onDestroy()
```

Start the service **before** `call.start()` and stop it when the call ends. The SDK is headless and deliberately doesn't impose a service (it has no notification UI) — that's your app's call. A foreground-only call works fine without any of this. Both voice example apps ship a complete, working `CallForegroundService` you can copy.

## R8 / ProGuard

**No keep rules needed in your app.** The `ai.poly:voice` AAR ships **consumer R8 rules** (applied automatically) that keep `org.webrtc.**` — libwebrtc is reached by name over JNI from native code, which R8 can't see, so stripping it would crash the audio engine. If you maintain a global `proguard-rules.pro` that's unusually aggressive, the shipped consumer rules still protect the SDK; you don't add anything.

## Example apps

A one-screen **tap-to-call** demo with the audio-output picker ships in both toolkits — drop your connector token and WebRTC token into the `PolyVoice.call(...)` block and run:

* [`examples/voice/compose`](https://github.com/polyai/android-sdk/tree/main/examples/voice/compose)
* [`examples/voice/views`](https://github.com/polyai/android-sdk/tree/main/examples/voice/views)

## Related pages

<CardGroup cols={2}>
  <Card title="Android SDK" icon="mobile" href="/messaging-channel/android-sdk">
    Chat with ai.poly:messaging: installation, authentication, sessions, and UI
  </Card>

  <Card title="Multichannel agents" icon="layer-group" href="/messaging-channel/multichannel">
    Build agents that work across voice, webchat, and mobile
  </Card>
</CardGroup>
