Voice SDK for Android
dm.sent:voice-client lets an Android app place Sent Voice calls: to a phone number, to another user of your app, or into a conference. Your backend mints a short-lived voice token for each user with the Sent API. The SDK registers the device with it, places calls, and reports how they go. What rings when a user calls is decided by your callback, as for every call; see Answering Calls.
This SDK places calls. Receiving calls on a phone needs push credentials registered with Sent, and arrives as a minor version of this SDK with no change to its shape.
Installation
From Maven Central, for Android 8.0 (API 26) or later:
dependencies {
implementation("dm.sent:voice-client:0.1.0")
}dm.sent:voice-client-core comes with it. Calls use the microphone: the library declares RECORD_AUDIO, and your app asks the user for it at runtime before the first call.
Quick Start
Mint voice tokens on your backend
Mint tokens on your server with your Sent API key, never in the app, for the user who is signed in:
// On your server, behind a route such as GET /api/voice-token
const response = await fetch('https://api.sent.dm/v3/voice/tokens', {
method: 'POST',
headers: { 'x-api-key': process.env.SENT_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: user.id }),
});
const { data } = await response.json();
return data.token;identity names the user in calls: letters, digits, - and _, up to 200 characters. The user is bound to your default voice number unless you pass number, one of the numbers you turned voice on for with POST /v3/channels/voice, and the token lasts ttl seconds (600 by default, at most 3600). Return the token as text from an endpoint of your own, such as /api/voice-token. The token is opaque: read the identity, number, and expiry from the minting response, not from the token. The token endpoint and the binding rules are covered in Calls From Your App.
Register and call
import dm.sent.voice.SentVoice
import dm.sent.voice.core.CallEvent
import dm.sent.voice.core.SentVoiceOptions
val client = SentVoice.client(applicationContext, SentVoiceOptions(tokenProvider = { myBackend.voiceToken() }))
// From a coroutine: register and connect suspend
client.register()
val call = client.connect("+14155551234")
val listener = call.on { event ->
when (event) {
is CallEvent.Connected -> Log.i("voice", "Connected")
is CallEvent.Disconnected -> Log.i("voice", "The call ended as ${event.state}")
else -> Unit
}
}Keep one client for the life of the app. register() fetches a token through tokenProvider, registers with it, and refreshes it before it expires. Events arrive on the thread that reported them, so hop to the main thread before touching views. A listener stays until you call cancel() on the token on returns.
Client Options
SentVoiceOptions takes the following options. Only tokenProvider is required.
| Option | Description |
|---|---|
tokenProvider | A suspend function that returns a voice token from your backend, which mints it with POST /v3/voice/tokens. Called by register() and again before each token expires |
registerRetries | How many more times registering or refreshing the token is retried after a temporary failure, such as a network error. Default 3 |
baseUrl | The Sent API origin the token exchange and telemetry use. Default https://api.sent.dm |
telemetryDisabled | Turns off the call quality and error reports the SDK sends to Sent. Default false |
log | A function that receives the SDK's log lines with their level. Voice tokens never appear in them |
Client
The client holds one registration and the call that runs on it.
| Property | Description |
|---|---|
state | UNREGISTERED, REGISTERING, REGISTERED, OFFLINE or DESTROYED. After destroy() the state is DESTROYED and every method throws |
identity | The identity the current token was minted for. null until registered |
number | The number the identity is bound to. null until registered |
activeCall | The call in progress, or null |
| Method | Description |
|---|---|
register() | Fetches a token from tokenProvider, registers with it, and keeps it refreshed. Returns once the client is REGISTERED |
unregister() | Drops the registration and leaves a call in progress alone |
destroy() | Ends the call and tears the client down for good |
connect(to) | Calls a phone number in E.164 format or another user of your app by identity. See Calls |
joinConference(name) | Joins one of your account's rooms by name |
on(listener) | Adds a listener for the events below and returns a token; cancel() on it removes the listener |
register(), unregister(), destroy(), connect() and joinConference() are suspend functions.
| Event | Fires |
|---|---|
ClientEvent.Registering | register() started |
ClientEvent.Registered | The client is registered and ready for calls |
ClientEvent.Unregistered | unregister() finished |
ClientEvent.ActiveCallChanged | A call was placed, or the call in progress ended |
ClientEvent.TokenWillExpire | The token is about to expire and the next refresh is due |
ClientEvent.Offline | The refresh retries ran out. The client tries again about every 30 seconds; register() tries at once |
ClientEvent.Error | Something failed outside a method call, such as a refresh |
Calls
connect(to) and joinConference(name) return a Call once the provider accepted it; the call then moves through RINGING, ANSWERED and CONNECTED to COMPLETED, FAILED, BUSY or NO_ANSWER. One call at a time: a second connect while one is in progress throws CALL_IN_PROGRESS.
| Property | Description |
|---|---|
id, direction, from, to, startedAtMs | What the call is |
state | The call's state, as above |
isMuted | Whether the microphone is muted |
connectedTimeMs, endedTimeMs | When the parties were joined, and when the call ended |
error | The exception that ended the call, when one did |
| Method | Description |
|---|---|
hangup() | Ends the call. A suspend function |
mute(muted) | Mutes or unmutes the microphone |
sendDigits(digits) | Sends DTMF digits, for example to a phone menu |
on(listener) | Adds a listener for the events below and returns a token |
| Event | Fires |
|---|---|
CallEvent.Ringing, Answered, Connected | The call progressed |
CallEvent.Reconnecting, Reconnected | The media path dropped and came back |
CallEvent.Disconnected | The call ended in state, with the error that ended it when there is one |
CallEvent.MuteChanged | mute() took effect |
CallEvent.Quality | The media quality degraded |
CallEvent.Error | Something failed on the call |
Errors
Every failure is a SentVoiceException with a code, a category, whether it is retriable, and a message you can show. The codes are the same on every platform:
| Code | Meaning |
|---|---|
TOKEN_INVALID | The value from tokenProvider is not a voice token, or Sent refused it |
TOKEN_EXPIRED | The token expired and could not be refreshed |
NOT_REGISTERED | A call was placed while the client was not REGISTERED |
MEDIA_PERMISSION_DENIED | The user refused the microphone |
INVALID_ADDRESS | to is neither an E.164 number nor an identity, or a room name is malformed |
CALL_IN_PROGRESS | A call was placed while one was in progress |
CALL_REJECTED | The other party, or your callback, declined the call |
CALL_FAILED | The call could not be placed or dropped. Calls to a phone number need a number on the token; the message says so when there is none |
NETWORK | Sent or the provider could not be reached. Retriable |
CAPABILITY_UNSUPPORTED | The device or the SDK version cannot do what was asked |
UNKNOWN | Anything else; message says what |
Telemetry
The SDK reports call quality and errors to Sent so support can see what a user saw, batched and sent in the background. Set telemetryDisabled to turn it off. Nothing in the reports identifies the person beyond the identity your backend minted the token for.