Skip to content

Commit e53acff

Browse files
elisaesKalam95salvodicara
authored
On call hold apicsj 1339 (#42)
* android holdCall * ios * ios pod lockfile * updated condition for hold ios * solving comments * solving comments * solving comments * Fixes for android * fix toggle button (android) * Fix formatting in iOS --------- Co-authored-by: Mehboob Alam <mehboob.alam@vonage.com> Co-authored-by: Salvatore Di Cara <salvodicara@gmail.com>
1 parent 85e000b commit e53acff

File tree

9 files changed

+193
-24
lines changed

9 files changed

+193
-24
lines changed

contact-center/android-voice/app/src/main/java/com/example/vonage/voicesampleapp/activities/CallActivity.kt

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ class CallActivity : AppCompatActivity() {
2323
private val coreContext = App.coreContext
2424
private val clientManager = coreContext.clientManager
2525
private var isMuteToggled = false
26+
private var isHoldToggled = false
27+
private var isNoiseSuppressionToggled = false
2628

2729
/**
2830
* When an Active Call gets disconnected
@@ -99,8 +101,9 @@ class CallActivity : AppCompatActivity() {
99101
btnReject.setOnClickListener { onReject() }
100102
btnHangup.setOnClickListener { onHangup() }
101103
btnMute.setOnClickListener { onMute() }
104+
btnHold.setOnClickListener {onHold()}
102105
btnKeypad.setOnClickListener { onKeypad() }
103-
noiseSuppressionSwitch.setOnCheckedChangeListener { _, isChecked -> onNoiseSuppression(isChecked) }
106+
btnNoiseSuppression.setOnClickListener { onNoiseSuppression() }
104107
}
105108

106109
private fun setUserUI() = binding.run{
@@ -116,16 +119,27 @@ class CallActivity : AppCompatActivity() {
116119
btnReject.visibility = View.VISIBLE
117120
btnHangup.visibility = View.GONE
118121
btnMute.visibility = View.GONE
122+
btnHold.visibility = View.GONE
119123
btnKeypad.visibility = View.GONE
120-
noiseSuppressionSwitch.visibility = View.GONE
124+
btnNoiseSuppression.visibility = View.GONE
121125
}
122126
else {
123127
btnAnswer.visibility = View.GONE
124128
btnReject.visibility = View.GONE
125129
btnHangup.visibility = View.VISIBLE
126130
btnMute.visibility = View.VISIBLE
131+
btnHold.visibility = View.VISIBLE
127132
btnKeypad.visibility = View.VISIBLE
128-
noiseSuppressionSwitch.visibility = View.VISIBLE
133+
btnNoiseSuppression.visibility = View.VISIBLE
134+
}
135+
// Buttons Toggled
136+
coreContext.activeCall?.run {
137+
if(isMuteToggled != isMuted && !isOnHold){
138+
toggleMute()
139+
}
140+
if(isHoldToggled != isOnHold){
141+
toggleHold()
142+
}
129143
}
130144
//Background Color and State label
131145
val (backgroundColor, stateLabel) = when(callStateExtra){
@@ -135,6 +149,7 @@ class CallActivity : AppCompatActivity() {
135149
Connection.STATE_RINGING -> R.color.gray to R.string.call_state_ringing_label
136150
Connection.STATE_DIALING -> R.color.gray to R.string.call_state_dialing_label
137151
Connection.STATE_ACTIVE -> R.color.green to R.string.call_state_active_label
152+
Connection.STATE_HOLDING -> R.color.gray to R.string.call_state_holding_label
138153
Connection.STATE_DISCONNECTED -> R.color.red to R.string.call_state_remotely_disconnected_label
139154
else -> R.color.red to R.string.call_state_locally_disconnected_label
140155
}
@@ -173,9 +188,18 @@ class CallActivity : AppCompatActivity() {
173188
}
174189
}
175190

176-
private fun onNoiseSuppression(isChecked: Boolean){
191+
private fun onHold(){
192+
coreContext.activeCall?.let { call ->
193+
if(toggleHold()){
194+
call.onHold()
195+
}else{
196+
call.onUnhold()
197+
}
198+
}
199+
}
200+
private fun onNoiseSuppression(){
177201
coreContext.activeCall?.let { call ->
178-
if(isChecked){
202+
if(toggleNoiseSuppression()){
179203
clientManager.enableNoiseSuppression(call)
180204
} else {
181205
clientManager.disableNoiseSuppression(call)
@@ -187,11 +211,21 @@ class CallActivity : AppCompatActivity() {
187211
showDialerFragment()
188212
}
189213

190-
private fun toggleMute() : Boolean{
214+
private fun toggleMute(): Boolean{
191215
isMuteToggled = binding.btnMute.toggleButton(isMuteToggled)
192216
return isMuteToggled
193217
}
194218

219+
private fun toggleHold(): Boolean {
220+
isHoldToggled = binding.btnHold.toggleButton(isHoldToggled)
221+
return isHoldToggled
222+
}
223+
224+
private fun toggleNoiseSuppression(): Boolean {
225+
isNoiseSuppressionToggled = binding.btnNoiseSuppression.toggleButton(isNoiseSuppressionToggled)
226+
return isNoiseSuppressionToggled
227+
}
228+
195229
private fun FloatingActionButton.toggleButton(toggle: Boolean): Boolean {
196230
backgroundTintList = ColorStateList.valueOf(getColor(if(!toggle) R.color.gray else R.color.white))
197231
imageTintList = ColorStateList.valueOf(getColor(if(!toggle) R.color.white else R.color.gray))
@@ -203,9 +237,11 @@ class CallActivity : AppCompatActivity() {
203237
const val IS_MUTED = "isMuted"
204238
const val CALL_STATE = "callState"
205239
const val CALL_ANSWERED = "answered"
240+
const val CALL_ON_HOLD = "holding"
206241
const val CALL_RECONNECTING = "reconnecting"
207242
const val CALL_RECONNECTED = "reconnected"
208243
const val CALL_DISCONNECTED = "disconnected"
209244
const val IS_REMOTE_DISCONNECT = "isRemoteDisconnect"
245+
210246
}
211247
}

contact-center/android-voice/app/src/main/java/com/example/vonage/voicesampleapp/core/VoiceClientManager.kt

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,14 @@ class VoiceClientManager(private val context: Context) {
142142
client.setOnMutedListener { callId, legId, isMuted ->
143143
println("LegId $legId for Call $callId has been ${if(isMuted) "muted" else "unmuted"}")
144144
takeIf { callId == legId } ?: return@setOnMutedListener
145-
// Update Active Call Mute State
146-
takeIfActive(callId)?.isMuted = isMuted
147-
// Notify Call Activity
148-
notifyIsMutedToCallActivity(context, isMuted)
145+
takeIfActive(callId)?.run {
146+
// Update Active Call Mute State
147+
toggleMuteState()
148+
takeUnless { it.isOnHold }?.run {
149+
// Notify Call Activity
150+
notifyIsMutedToCallActivity(context, isMuted)
151+
}
152+
}
149153
}
150154

151155
client.setOnDTMFListener { callId, legId, digits ->
@@ -365,6 +369,46 @@ class VoiceClientManager(private val context: Context) {
365369
}
366370
}
367371

372+
fun holdCall(call: CallConnection){
373+
call.takeIfActive()?.apply{
374+
client.mute(callId){ error ->
375+
error?.let {
376+
println("Error muting in holdCall with id: $callId")
377+
} ?: run {
378+
client.enableEarmuff(callId){ error ->
379+
error?.let {
380+
println("Error enabling earmuff in holdCall with id: $callId")
381+
} ?: run {
382+
println("Call $callId successfully put on hold")
383+
toggleHoldState()
384+
notifyIsOnHoldToCallActivity(context, true)
385+
}
386+
}
387+
}
388+
}
389+
}
390+
}
391+
392+
fun unholdCall(call: CallConnection){
393+
call.takeIfActive()?.apply{
394+
client.unmute(callId){ error ->
395+
error?.let {
396+
println("Error unmuting in unholdCall with id: $callId")
397+
} ?: run {
398+
client.disableEarmuff(callId){ error ->
399+
error?.let {
400+
println("Error disabling earmuff in unholdCall with id: $callId")
401+
} ?: run {
402+
println("Call $callId successfully removed from hold")
403+
toggleHoldState()
404+
notifyIsOnHoldToCallActivity(context, false)
405+
}
406+
}
407+
}
408+
}
409+
}
410+
}
411+
368412
/*
369413
* Utilities to handle errors on telecomHelper
370414
*/

contact-center/android-voice/app/src/main/java/com/example/vonage/voicesampleapp/telecom/CallConnection.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ class CallConnection(val callId: CallId) : Connection() {
1414
private val coreContext = App.coreContext
1515
private val clientManager = coreContext.clientManager
1616
var isMuted = false
17+
private set
18+
var isOnHold = false
19+
private set
20+
1721
init {
1822
val properties = connectionProperties or PROPERTY_SELF_MANAGED
1923
connectionProperties = properties
@@ -60,6 +64,18 @@ class CallConnection(val callId: CallId) : Connection() {
6064
clientManager.sendDtmf(this, c.toString())
6165
}
6266

67+
override fun onHold() {
68+
if(!isOnHold){
69+
clientManager.holdCall(this)
70+
}
71+
}
72+
73+
override fun onUnhold() {
74+
if(isOnHold){
75+
clientManager.unholdCall(this)
76+
}
77+
}
78+
6379
fun selfDestroy(){
6480
println("[$callId] Connection is no more useful, destroying it")
6581
setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
@@ -77,6 +93,15 @@ class CallConnection(val callId: CallId) : Connection() {
7793
coreContext.activeCall = coreContext.activeCall ?: this
7894
}
7995

96+
fun toggleHoldState(){
97+
isOnHold = !isOnHold
98+
if(isOnHold) setOnHold() else setActive()
99+
}
100+
101+
fun toggleMuteState(){
102+
isMuted = !isMuted
103+
}
104+
80105
private fun clearActiveCall(){
81106
// Reset active call only if it was the current one
82107
coreContext.activeCall?.takeIf { it == this }?.let { coreContext.activeCall = null }

contact-center/android-voice/app/src/main/java/com/example/vonage/voicesampleapp/utils/NavigationUtils.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ internal fun notifyIsMutedToCallActivity(context: Context, isMuted: Boolean){
7575
sendMessageToCallActivity(context, extras)
7676
}
7777

78+
internal fun notifyIsOnHoldToCallActivity(context: Context, isOnHold: Boolean){
79+
val extras = Bundle()
80+
val state = if(isOnHold) CallActivity.CALL_ON_HOLD else CallActivity.CALL_ANSWERED
81+
extras.putString(CallActivity.CALL_STATE, state)
82+
sendMessageToCallActivity(context, extras)
83+
}
84+
7885
internal fun notifyCallAnsweredToCallActivity(context: Context) {
7986
val extras = Bundle()
8087
extras.putString(CallActivity.CALL_STATE, CallActivity.CALL_ANSWERED)
Loading
Loading

contact-center/android-voice/app/src/main/res/layout/activity_call.xml

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
android:orientation="horizontal"
6161
android:gravity="center"
6262
android:layout_width="match_parent"
63-
android:layout_height="100dp">
63+
android:layout_height="match_parent">
6464
<com.google.android.material.floatingactionbutton.FloatingActionButton
6565
android:layout_width="wrap_content"
6666
android:layout_height="wrap_content"
@@ -95,7 +95,17 @@
9595
android:layout_width="wrap_content"
9696
android:layout_height="wrap_content"
9797
app:fabSize="normal"
98-
android:layout_marginStart="24dp"
98+
android:layout_marginStart="10dp"
99+
android:id="@+id/btn_hold"
100+
app:backgroundTint="@color/white"
101+
app:tint="@color/gray"
102+
app:srcCompat="@drawable/hold"
103+
android:contentDescription="@string/hold_button_description"/>
104+
<com.google.android.material.floatingactionbutton.FloatingActionButton
105+
android:layout_width="wrap_content"
106+
android:layout_height="wrap_content"
107+
app:fabSize="normal"
108+
android:layout_marginStart="10dp"
99109
android:layout_marginTop="16dp"
100110
android:id="@+id/btn_hangup"
101111
app:backgroundTint="@color/red"
@@ -106,21 +116,23 @@
106116
android:layout_width="wrap_content"
107117
android:layout_height="wrap_content"
108118
app:fabSize="normal"
109-
android:layout_marginStart="24dp"
119+
android:layout_marginStart="10dp"
120+
android:id="@+id/btn_noise_suppression"
121+
app:backgroundTint="@color/white"
122+
app:tint="@color/gray"
123+
app:srcCompat="@drawable/noise_suppression"
124+
android:contentDescription="@string/noise_suppression_button_description"/>
125+
<com.google.android.material.floatingactionbutton.FloatingActionButton
126+
android:layout_width="wrap_content"
127+
android:layout_height="wrap_content"
128+
app:fabSize="normal"
129+
android:layout_marginStart="10dp"
110130
android:id="@+id/btn_keypad"
111131
app:backgroundTint="@color/white"
112132
app:tint="@color/gray"
113133
app:srcCompat="@drawable/keypad"
114134
android:contentDescription="@string/keypad_button_description"/>
115135
</LinearLayout>
116-
<androidx.appcompat.widget.SwitchCompat
117-
android:id="@+id/noise_suppression_switch"
118-
android:layout_below="@+id/call_actions"
119-
android:layout_centerHorizontal="true"
120-
android:layout_width="wrap_content"
121-
android:layout_height="wrap_content"
122-
app:switchPadding="5dp"
123-
android:text="@string/noise_suppression_switch_label"/>
124136
</RelativeLayout>
125137
<FrameLayout
126138
android:background="?attr/colorOnPrimary"

contact-center/android-voice/app/src/main/res/values/strings.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<string name="call_state_reconnecting_label">Reconnecting…</string>
3131
<string name="keypad_button_description">Keypad</string>
3232
<string name="mute_button_description">Mute</string>
33+
<string name="hold_button_description">Hold</string>
3334
<string name="hangup_button_description">Hangup</string>
3435
<string name="dismiss_button_content_description">Dismiss</string>
3536
<string name="logged_username_default">Logged Username</string>
@@ -39,5 +40,6 @@
3940
<string name="answer_button_description">Answer</string>
4041
<string name="reject_button_description">Reject</string>
4142
<string name="login_with_token_label">Login with Vonage Token</string>
42-
<string name="noise_suppression_switch_label">Noise Suppression</string>
43+
<string name="noise_suppression_button_description">Noise Suppression</string>
44+
<string name="call_state_holding_label">On Hold</string>
4345
</resources>

contact-center/ios-voice/VonageSDKClientVOIPExample/Controllers/Calls/CallController+CXProviderDelegate.swift

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,42 @@ extension VonageCallController: CXProviderDelegate {
8484
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession){
8585
VGVoiceClient.disableAudio(audioSession)
8686
}
87+
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction){
88+
guard let _ = self.vonageActiveCalls.value[action.callUUID] else {
89+
action.fail()
90+
return
91+
}
92+
let callId = action.callUUID.toVGCallID()
93+
if (action.isOnHold) {
94+
self.client.mute(callId) { error in
95+
if let error = error {
96+
// TODO:
97+
return
98+
}
99+
self.client.enableEarmuff(callId) { error in
100+
if let error = error {
101+
// TODO:
102+
return
103+
}
104+
}
105+
}
106+
} else {
107+
self.client.unmute(callId) { error in
108+
if let error = error {
109+
// TODO:
110+
return
111+
}
112+
self.client.disableEarmuff(callId) { error in
113+
if let error = error {
114+
// TODO:
115+
return
116+
}
117+
}
118+
}
119+
}
120+
// CallKit requires to fulfill the action synchronously
121+
action.fulfill()
122+
}
87123
}
88124

89125
extension VonageCallController {
@@ -114,6 +150,13 @@ extension VonageCallController {
114150
case .answered:
115151
// Answers are remote by definition, so report them
116152
self.callProvider.reportOutgoingCall(with: callId, connectedAt: Date.now)
153+
let update = CXCallUpdate()
154+
update.localizedCallerName = "me"
155+
update.supportsDTMF = true
156+
update.supportsHolding = true
157+
update.supportsGrouping = false
158+
update.hasVideo = false
159+
self.callProvider.reportCall(with: callId, updated: update)
117160

118161
case .completed(true, .some(let reason)):
119162
// Report Remote Hangups + Cancels
@@ -130,8 +173,8 @@ extension VonageCallController {
130173
// Report new Inbound calls so we follow PushKit and Callkit Rules
131174
let update = CXCallUpdate()
132175
update.localizedCallerName = from
133-
update.supportsDTMF = false
134-
update.supportsHolding = false
176+
update.supportsDTMF = true
177+
update.supportsHolding = true
135178
update.supportsGrouping = false
136179
update.hasVideo = false
137180
self.callProvider.reportNewIncomingCall(with: callId, update: update) { err in

0 commit comments

Comments
 (0)