Danamon Enhancement Mobile Development Guide - Android
Attached data fields on the create call
Each EnvironmentCallAttribute declared for an environment becomes one input on the
create call page, and its value is sent as an entry of DialCallParameters.attributes
(the Attributes object of the dialex request).
| Key | Label | Values |
|---|---|---|
video_flow |
Video Flow | ETB, NTB |
video_channel |
Video Channel | Registration, Reactivation, Activation, Saving (2.0), Credit Card |
video_application |
Video Application | DBP 2.0 |
video_language |
Video Language | id, en |
Backward compatibility. Older clients do not send these attributes at all. The
service fills in its own defaults for them (video_flow = NTB,
video_channel = Saving (1.0), video_application = DBP 1.0, video_language = id).
The 1.0 values exist only as those service side defaults, which is why they are not
offered on the create call page — a current client always sends the values explicitly.
Showing Queue Position and Estimated Wait Time
The SDK can report the caller's live queue position and estimated wait time (EWT) while the call is waiting in the queue. It is off by default; enable it with one configuration flag and listen for one event.
What the SDK does
The SDK has three mutually exclusive ways of waiting for the queue. They are picked in a fixed order of precedence:
| # | Mode | Enabled by | Reports position / wait time |
|---|---|---|---|
| 1 | Queue socket | useQueueSocket = true |
no |
| 2 | Queue info polling | useQueueSocket = false and useQueueInfo = true |
yes |
| 3 | Legacy waitforroominterval polling |
both flags false | no |
In queue info mode the SDK polls /api/WaitForRoomQueueInterval and emits a
QueueInfoUpdated event on every poll. It still raises AgentConnected, CallEnded
and Error exactly as before — nothing else in your call flow changes.
Position and wait time come from the Genesys URS statistics through the service.
Enable the feature
val sdkConfig = ECVSdkConfiguration(
serviceUrl = "https://your-ecv-service",
useQueueSocket = false, // the queue socket takes precedence, so turn it off
useQueueInfo = true, // queue position + estimated wait time
queueInfoIntervalDelay = 3000 // poll interval in ms, optional (default 3000)
)
Java:
ECVSdkConfiguration config = ECVSdkConfiguration.builder("https://your-ecv-service")
.useQueueSocket(false)
.useQueueInfo(true)
.build();
useQueueInfois ignored whileuseQueueSocketis true. If you leave the queue socket on, the call still works — you simply get noQueueInfoUpdatedevents.
Per-call override
Both flags can also be set per call, on the VideoCall object. A null override (the
default) falls back to the SDK configuration; a non-null value wins for that call only.
Set them after createCall() and before start() — they are read when the queue
wait begins:
val call = sdkInstance.createCall()
call.useQueueSocketOverride = false // this call: no socket
call.useQueueInfoOverride = true // this call: queue info polling
call.start(params)
Java:
VideoCall call = sdkInstance.createCall();
call.setUseQueueSocketOverride(false);
call.setUseQueueInfoOverride(true);
call.start(params);
The same precedence applies to the effective values: socket > queue info > legacy. So to
get queue info mode for a single call while the global configuration uses the socket,
override both flags as above — overriding only useQueueInfoOverride is not enough,
because the socket still wins.
If your service exposes the endpoint under a different path, override it with
queueInfoEndpoint (default /api/WaitForRoomQueueInterval?rtcId=).
In the demo both flags are per environment. They are declared in
SerializableECVSdkConfiguration
(DEMO/src/main/java/group/ccr/ecv175_android/data/Environment.kt) and set per
environment in DEMO/build.gradle.kts:
createEnvironmentGradle(
name = "…",
serviceUrl = "…",
useQueueSocket = false,
useQueueInfo = true,
…
)
Backend requirement: the service must expose
GET /api/WaitForRoomQueueInterval?rtcId=…. Position and wait time are only filled in when Genesys URS returns queue statistics; otherwise they arrive asnulland the UI should fall back to a plain spinner.
Listen for the event
QueueInfoUpdated carries a QueueInfo in objData:
videoCall.eventFlow.collect { event ->
when (event.type) {
CallEventTypes.QueueInfoUpdated -> {
queueInfo = event.objData as? QueueInfo
}
CallEventTypes.AgentConnected -> { /* navigate to the call screen */ }
CallEventTypes.Error,
CallEventTypes.Hangup,
CallEventTypes.CallEnded -> { /* leave the queue screen */ }
else -> {}
}
}
The last snapshot is also available at any time as videoCall.queueInfo, which is
useful when the screen is composed after the first event was already emitted:
var queueInfo by remember { mutableStateOf(videoCall.queueInfo) }
QueueInfo
| Member | Type | Meaning |
|---|---|---|
state |
String | Raw wait state (wait, not found, agent_connected, …). The SDK already acts on it; you normally do not need it. |
position |
Int? | Raw position. null if the service has no queue info, -1 if URS reported nothing for the interaction (see below). |
ewtSeconds |
Double? | Raw estimated wait time in seconds. Same null / -1 rules. |
displayPosition |
Int? | position filtered for UI — null unless it is a real position. |
ewtMinutes |
Int? | ewtSeconds rounded up to whole minutes, null when unknown. |
Prefer displayPosition and ewtMinutes in the UI.
What -1 means
position = -1 together with ewtSeconds = -1 is the service's "no queue data"
answer. It is not a call state — read state (or the SDK's own events) for that.
It is returned whenever URS has nothing to report for the interaction, which covers
several different situations:
- the interaction left the queue and is already routed and ringing at an agent — URS no longer tracks it as queued, but the call is perfectly alive
- the interaction never reached URS, or the service could not resolve its connection id
- the queue statistics mode configured on the service does not match the queue type, so the URS lookup fails
- the call really did end on the Genesys side
Because these are indistinguishable from the client, never treat -1 as "the call
ended" and never end the call because of it. Termination always arrives through the
state field and the SDK's CallEnded / Hangup / Error events.
-1 is also perfectly normal while queued: the log below is a caller who is still
waiting, with URS simply not publishing figures for that queue.
Queue info: state=wait position=-1 ewt=-1.0
displayPosition and ewtMinutes both return null for -1, so UI code that uses
them needs no special handling.
4. Render it
Queue figures are not guaranteed to arrive, so the demo keeps its original waiting
screen and only switches to the position layout once there is something real to show.
From DEMO/src/main/java/group/ccr/ecv175_android/ui/QueueScreen.kt:
val info = queueInfo
// Only switch layouts once the backend actually reported something to show.
val hasQueueData = info != null && (info.displayPosition != null || info.ewtMinutes != null)
if (hasQueueData) {
QueueInfoWaiting(info!!, innerPadding, onCancel = { videoCall.hangup() })
} else {
PlainWaiting(innerPadding, onCancel = { videoCall.hangup() }) // spinner only
}
Inside the position layout each figure is rendered only when it exists, since a queue can publish a position without an EWT or the other way round:
info.displayPosition?.let { position ->
Text("$position", fontSize = 64.sp, fontWeight = FontWeight.Bold)
}
info.ewtMinutes?.let { minutes ->
Text("Estimated wait time about $minutes minutes")
}
Design the screen so it works with no figures at all. The first polls happen before
the interaction reaches Genesys, some routing strategies never publish an EWT, and a
caller who gets routed mid-wait goes back to -1 (see above). A layout that assumes a
number is always present will show an empty box for the whole call on such a queue.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
No QueueInfoUpdated events at all |
Feature not enabled, or the queue socket is still on | Check useQueueInfo = true and useQueueSocket = false — in the configuration, or as per-call overrides set before start() |
Events arrive with state=wait but position=-1 ewt=-1.0 for the whole call |
URS reported nothing for this interaction | Check the service log for the urs/call/… request, and whether the queue statistics mode configured on the service matches the queue type. The screen stays on the plain waiting layout, which is the intended fallback. |
position / ewtSeconds flip to -1 mid-wait |
The interaction left the queue — usually routed and now ringing at an agent | Expected, and not a call end. Keep waiting for AgentConnected. |
| Queue never progresses | rtcId mismatch or the dial failed |
Compare the rtcId in the app log with the dialex request on the service |
Hold and Resume
When the agent puts the call on hold from the agent desktop, the SDK reports it to your application so you can cover the screen with a "please wait" overlay, and reports the resume so you can take it away again.
No configuration is needed — the events are always emitted.
How it works
Hold is driven entirely by the agent side. The agent desktop sends a control message over the WebRTC data channel, and the SDK turns it into an event:
| Agent action | Data channel message | SDK event |
|---|---|---|
| Puts the call on hold | HOLD|… |
CallEventTypes.StartHold |
| Takes the call back | PICKUP|… |
CallEventTypes.EndHold |
There is no client-side API to place a call on hold — the caller cannot hold the agent. Your job is to react to the two events.
Implement the overlay
Three pieces: a flag, two event branches, and a composable rendered on top.
Keep a flag
From DEMO/src/main/java/group/ccr/ecv175_android/ui/CallScreen.kt:
var isHeld by remember { mutableStateOf(false) }
Drive it from the events
videoCall.eventFlow.collect { event ->
when (event.type) {
CallEventTypes.StartHold -> isHeld = true
CallEventTypes.EndHold -> isHeld = false
// … other events …
else -> {}
}
}
Java, using the lifecycle-aware helper:
videoCall.subscribe(this, event -> {
if (event.getType() == CallEventTypes.StartHold) {
showHoldOverlay();
} else if (event.getType() == CallEventTypes.EndHold) {
hideHoldOverlay();
}
});
Render the overlay last
Inside the Box that holds the video views and the call controls, render the overlay
after everything else so it paints on top:
// --- OVERLAYS ---
// These are rendered last to appear on top of the UI elements above.
if (isOnHold) {
HoldOverlay() // Full screen overlay, zIndex 4
}
@Composable
private fun HoldOverlay() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
.zIndex(4f), // Above controls
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.HourglassEmpty,
contentDescription = "Call on Hold",
modifier = Modifier.size(64.dp),
tint = Color.White
)
}
}
Things to get right
Make it opaque. The overlay is a solid black Box, not a translucent scrim. The
agent side stops sending video for the duration of the hold, so what sits behind the
overlay is a dead surface — a frozen last frame or a blank view, depending on the device.
An opaque overlay replaces that with a deliberate "please wait" state; a translucent
scrim would just show the caller a dimmed frozen picture of the agent.
Render it above the controls. zIndex(4f) puts it over the call control row. In the
demo this means the caller cannot hang up while on hold. That is a deliberate choice — if
your flow needs an escape hatch, either lower the overlay's zIndex below the controls or
place a hangup button inside the overlay itself.
The state is per call. isHeld lives in the call screen's composition, so a call that
ends while on hold simply disposes it. There is nothing to reset.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
No StartHold / EndHold events |
The data channel is not up, or the agent desktop does not send hold messages | Check for TextChannelConnected in the log; verify hold works from the agent desktop for other clients |
| Overlay stays after the agent resumes | EndHold handled on a collector that was cancelled |
Collect the events for as long as the call screen is composed |
No comments to display
No comments to display