@qusaieilouti99/call-manager 0.1.50 → 0.1.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,6 +13,7 @@ import android.graphics.Color
13
13
  import android.media.AudioAttributes
14
14
  import android.media.AudioDeviceCallback
15
15
  import android.media.AudioDeviceInfo
16
+ import android.media.AudioFocusRequest
16
17
  import android.media.AudioManager
17
18
  import android.media.MediaPlayer
18
19
  import android.media.RingtoneManager
@@ -53,6 +54,7 @@ object CallEngine {
53
54
  private var audioManager: AudioManager? = null
54
55
  private var wakeLock: PowerManager.WakeLock? = null
55
56
  private var appContext: Context? = null
57
+ private var audioFocusRequest: AudioFocusRequest? = null
56
58
 
57
59
  // Call State Management
58
60
  private val activeCalls = ConcurrentHashMap<String, CallInfo>()
@@ -63,6 +65,11 @@ object CallEngine {
63
65
  // Audio State Tracking
64
66
  private var lastAudioRoutesInfo: AudioRoutesInfo? = null
65
67
  private var lastMuteState: Boolean = false
68
+ private var hasAudioFocus: Boolean = false
69
+
70
+ // System Call State Tracking
71
+ private var isSystemCallActive: Boolean = false
72
+ private var wasHeldBySystem: Boolean = false
66
73
 
67
74
  // Lock Screen Bypass
68
75
  private var lockScreenBypassActive = false
@@ -80,19 +87,147 @@ object CallEngine {
80
87
  val callData: String,
81
88
  var state: CallState,
82
89
  val callType: String = "Audio",
83
- val timestamp: Long = System.currentTimeMillis()
90
+ val timestamp: Long = System.currentTimeMillis(),
91
+ var wasHeldBySystem: Boolean = false
84
92
  )
85
93
 
86
94
  enum class CallState {
87
- INCOMING, DIALING, ACTIVE, HELD, ENDED
95
+ INCOMING, DIALING, ACTIVE, HELD, HELD_BY_SYSTEM, ENDED
88
96
  }
89
97
 
90
98
  interface LockScreenBypassCallback {
91
99
  fun onLockScreenBypassChanged(shouldBypass: Boolean)
92
100
  }
93
101
 
94
- interface ServerCallRejectCallback {
95
- fun onRejectCall(callId: String, reason: String)
102
+ // --- Audio Focus Management ---
103
+ private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
104
+ Log.d(TAG, "Audio focus changed: $focusChange")
105
+ when (focusChange) {
106
+ AudioManager.AUDIOFOCUS_LOSS -> {
107
+ // Lost focus permanently - likely due to phone call
108
+ handleAudioFocusLoss()
109
+ }
110
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
111
+ // Lost focus temporarily
112
+ handleAudioFocusLossTransient()
113
+ }
114
+ AudioManager.AUDIOFOCUS_GAIN -> {
115
+ // Regained focus
116
+ handleAudioFocusGain()
117
+ }
118
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
119
+ // Can duck audio
120
+ handleAudioFocusLossCanDuck()
121
+ }
122
+ }
123
+ }
124
+
125
+ private fun handleAudioFocusLoss() {
126
+ Log.d(TAG, "Audio focus lost permanently - likely system call active")
127
+ hasAudioFocus = false
128
+ isSystemCallActive = true
129
+
130
+ // Hold all active calls instead of ending them
131
+ activeCalls.values.filter { it.state == CallState.ACTIVE }.forEach { call ->
132
+ if (!call.wasHeldBySystem) {
133
+ call.wasHeldBySystem = true
134
+ call.state = CallState.HELD_BY_SYSTEM
135
+
136
+ val connection = telecomConnections[call.callId]
137
+ connection?.setOnHold()
138
+
139
+ emitEvent(CallEventType.CALL_HELD, JSONObject().apply {
140
+ put("callId", call.callId)
141
+ put("reason", "system_call_active")
142
+ })
143
+
144
+ notifySpecificCallStateChanged(appContext!!, call.callId, CallState.HELD_BY_SYSTEM)
145
+ Log.d(TAG, "Call ${call.callId} held by system due to audio focus loss")
146
+ }
147
+ }
148
+
149
+ stopRingback()
150
+ updateForegroundNotification(appContext!!)
151
+ }
152
+
153
+ private fun handleAudioFocusLossTransient() {
154
+ // Similar to permanent loss but may be temporary
155
+ handleAudioFocusLoss()
156
+ }
157
+
158
+ private fun handleAudioFocusGain() {
159
+ Log.d(TAG, "Audio focus regained - system call likely ended")
160
+ hasAudioFocus = true
161
+ isSystemCallActive = false
162
+
163
+ // Resume calls that were held by system
164
+ activeCalls.values.filter { it.state == CallState.HELD_BY_SYSTEM && it.wasHeldBySystem }.forEach { call ->
165
+ call.wasHeldBySystem = false
166
+ call.state = CallState.ACTIVE
167
+
168
+ val connection = telecomConnections[call.callId]
169
+ connection?.setActive()
170
+
171
+ emitEvent(CallEventType.CALL_UNHELD, JSONObject().apply {
172
+ put("callId", call.callId)
173
+ put("reason", "system_call_ended")
174
+ })
175
+
176
+ notifySpecificCallStateChanged(appContext!!, call.callId, CallState.ACTIVE)
177
+ Log.d(TAG, "Call ${call.callId} resumed after system call ended")
178
+ }
179
+
180
+ updateForegroundNotification(appContext!!)
181
+ }
182
+
183
+ private fun handleAudioFocusLossCanDuck() {
184
+ // Lower volume but continue
185
+ Log.d(TAG, "Audio focus loss - can duck")
186
+ }
187
+
188
+ private fun requestAudioFocus(): Boolean {
189
+ audioManager = audioManager ?: appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
190
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
191
+ if (audioFocusRequest == null) {
192
+ audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
193
+ .setAudioAttributes(
194
+ AudioAttributes.Builder()
195
+ .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
196
+ .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
197
+ .build()
198
+ )
199
+ .setOnAudioFocusChangeListener(audioFocusChangeListener)
200
+ .build()
201
+ }
202
+ val result = audioManager?.requestAudioFocus(audioFocusRequest!!)
203
+ hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
204
+ Log.d(TAG, "Audio focus request result: $result")
205
+ hasAudioFocus
206
+ } else {
207
+ @Suppress("DEPRECATION")
208
+ val result = audioManager?.requestAudioFocus(
209
+ audioFocusChangeListener,
210
+ AudioManager.STREAM_VOICE_CALL,
211
+ AudioManager.AUDIOFOCUS_GAIN
212
+ )
213
+ hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
214
+ Log.d(TAG, "Audio focus request result (legacy): $result")
215
+ hasAudioFocus
216
+ }
217
+ }
218
+
219
+ private fun abandonAudioFocus() {
220
+ audioManager = audioManager ?: appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
221
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
222
+ audioFocusRequest?.let { request ->
223
+ audioManager?.abandonAudioFocusRequest(request)
224
+ }
225
+ } else {
226
+ @Suppress("DEPRECATION")
227
+ audioManager?.abandonAudioFocus(audioFocusChangeListener)
228
+ }
229
+ hasAudioFocus = false
230
+ Log.d(TAG, "Audio focus abandoned")
96
231
  }
97
232
 
98
233
  // --- Event System ---
@@ -176,6 +311,7 @@ object CallEngine {
176
311
  obj.put("callData", it.callData)
177
312
  obj.put("state", it.state.name)
178
313
  obj.put("callType", it.callType)
314
+ obj.put("wasHeldBySystem", it.wasHeldBySystem)
179
315
  jsonArray.put(obj)
180
316
  }
181
317
  val result = jsonArray.toString()
@@ -192,15 +328,13 @@ object CallEngine {
192
328
  val incomingCall = activeCalls.values.find { it.state == CallState.INCOMING }
193
329
  if (incomingCall != null && incomingCall.callId != callId) {
194
330
  Log.d(TAG, "Incoming call collision detected. Auto-rejecting new call: $callId")
195
-
196
- // Auto-reject the new call
197
331
  rejectIncomingCallCollision(callId, "Another call is already incoming")
198
332
  return
199
333
  }
200
334
 
201
335
  // Check if there's an active call when receiving incoming
202
- val activeCall = activeCalls.values.find { it.state == CallState.ACTIVE }
203
- if (activeCall != null) {
336
+ val activeCall = activeCalls.values.find { it.state == CallState.ACTIVE || it.state == CallState.HELD_BY_SYSTEM }
337
+ if (activeCall != null && !canMakeMultipleCalls) {
204
338
  Log.d(TAG, "Active call exists when receiving incoming call. Auto-rejecting: $callId")
205
339
  rejectIncomingCallCollision(callId, "Another call is already active")
206
340
  return
@@ -212,7 +346,11 @@ object CallEngine {
212
346
 
213
347
  if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
214
348
  Log.d(TAG, "Can't make multiple calls, holding existing calls.")
215
- activeCalls.values.forEach { it.state = CallState.HELD }
349
+ activeCalls.values.forEach {
350
+ if (it.state == CallState.ACTIVE) {
351
+ it.state = CallState.HELD
352
+ }
353
+ }
216
354
  }
217
355
 
218
356
  activeCalls[callId] = CallInfo(callId, callData, CallState.INCOMING, parsedCallType)
@@ -266,7 +404,11 @@ object CallEngine {
266
404
 
267
405
  if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
268
406
  Log.d(TAG, "Can't make multiple calls, holding existing calls before outgoing.")
269
- activeCalls.values.forEach { it.state = CallState.HELD }
407
+ activeCalls.values.forEach {
408
+ if (it.state == CallState.ACTIVE) {
409
+ it.state = CallState.HELD
410
+ }
411
+ }
270
412
  }
271
413
 
272
414
  activeCalls[callId] = CallInfo(callId, callData, CallState.DIALING, parsedCallType)
@@ -290,10 +432,14 @@ object CallEngine {
290
432
  startForegroundService(context)
291
433
  Log.d(TAG, "Successfully reported outgoing call to TelecomManager via placeCall for $callId")
292
434
 
293
- startRingback()
435
+ // Request audio focus for outgoing call
436
+ if (requestAudioFocus()) {
437
+ startRingback()
438
+ setInitialAudioRoute(context, parsedCallType)
439
+ }
440
+
294
441
  bringAppToForeground(context)
295
442
  keepScreenAwake(context, true)
296
- setInitialAudioRoute(context, parsedCallType)
297
443
  } catch (e: SecurityException) {
298
444
  Log.e(TAG, "SecurityException: Failed to start outgoing call via placeCall. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
299
445
  endCall(context, callId)
@@ -331,12 +477,21 @@ object CallEngine {
331
477
  stopRingback()
332
478
  cancelIncomingCallUI(context)
333
479
 
480
+ // Request audio focus when answering
481
+ if (!hasAudioFocus) {
482
+ requestAudioFocus()
483
+ }
484
+
334
485
  // Update call state
335
486
  activeCalls[callId]?.state = CallState.ACTIVE
336
487
  currentCallId = callId
337
488
 
338
489
  if (!canMakeMultipleCalls) {
339
- activeCalls.filter { it.key != callId }.values.forEach { it.state = CallState.HELD }
490
+ activeCalls.filter { it.key != callId }.values.forEach {
491
+ if (it.state == CallState.ACTIVE) {
492
+ it.state = CallState.HELD
493
+ }
494
+ }
340
495
  }
341
496
 
342
497
  // Bring app to foreground when call is answered
@@ -347,6 +502,9 @@ object CallEngine {
347
502
 
348
503
  updateLockScreenBypass()
349
504
 
505
+ // Update foreground notification with call info
506
+ updateForegroundNotification(context)
507
+
350
508
  // Emit event with full call data instead of just callId
351
509
  emitEvent(CallEventType.CALL_ANSWERED, JSONObject().apply {
352
510
  put("callId", callId)
@@ -363,7 +521,7 @@ object CallEngine {
363
521
  Log.d(TAG, "holdCall: $callId")
364
522
  val callInfo = activeCalls[callId]
365
523
  if (callInfo?.state != CallState.ACTIVE) {
366
- Log.w(TAG, "Cannot hold call $callId - not in active state")
524
+ Log.w(TAG, "Cannot hold call $callId - not in active state (current: ${callInfo?.state})")
367
525
  return
368
526
  }
369
527
 
@@ -380,12 +538,35 @@ object CallEngine {
380
538
  fun unholdCall(context: Context, callId: String) {
381
539
  Log.d(TAG, "unholdCall: $callId")
382
540
  val callInfo = activeCalls[callId]
383
- if (callInfo?.state != CallState.HELD) {
384
- Log.w(TAG, "Cannot unhold call $callId - not in held state")
541
+ if (callInfo?.state != CallState.HELD && callInfo?.state != CallState.HELD_BY_SYSTEM) {
542
+ Log.w(TAG, "Cannot unhold call $callId - not in held state (current: ${callInfo?.state})")
385
543
  return
386
544
  }
387
545
 
546
+ // If call was held by system, don't allow manual unhold until system allows it
547
+ if (callInfo.state == CallState.HELD_BY_SYSTEM && isSystemCallActive) {
548
+ Log.w(TAG, "Cannot unhold call $callId - held by system and system call still active")
549
+ emitEvent(CallEventType.CALL_UNHOLD_FAILED, JSONObject().apply {
550
+ put("callId", callId)
551
+ put("reason", "system_call_active")
552
+ })
553
+ return
554
+ }
555
+
556
+ // Request audio focus before unholding
557
+ if (!hasAudioFocus) {
558
+ if (!requestAudioFocus()) {
559
+ Log.w(TAG, "Cannot unhold call $callId - failed to gain audio focus")
560
+ emitEvent(CallEventType.CALL_UNHOLD_FAILED, JSONObject().apply {
561
+ put("callId", callId)
562
+ put("reason", "audio_focus_failed")
563
+ })
564
+ return
565
+ }
566
+ }
567
+
388
568
  activeCalls[callId]?.state = CallState.ACTIVE
569
+ activeCalls[callId]?.wasHeldBySystem = false
389
570
  val connection = telecomConnections[callId]
390
571
  connection?.setActive()
391
572
 
@@ -619,6 +800,7 @@ object CallEngine {
619
800
  audioManager?.stopBluetoothSco()
620
801
  audioManager?.isBluetoothScoOn = false
621
802
  audioManager?.isSpeakerphoneOn = false
803
+ abandonAudioFocus()
622
804
  } else {
623
805
  Log.d(TAG, "Audio mode not reset; ${activeCalls.size} calls still active.")
624
806
  }
@@ -697,7 +879,9 @@ object CallEngine {
697
879
  fun isCallActive(): Boolean = activeCalls.any {
698
880
  it.value.state == CallState.ACTIVE ||
699
881
  it.value.state == CallState.INCOMING ||
700
- it.value.state == CallState.DIALING
882
+ it.value.state == CallState.DIALING ||
883
+ it.value.state == CallState.HELD ||
884
+ it.value.state == CallState.HELD_BY_SYSTEM
701
885
  }
702
886
 
703
887
  private fun validateOutgoingCallRequest(): Boolean {
@@ -727,8 +911,6 @@ object CallEngine {
727
911
  CoroutineScope(Dispatchers.IO).launch {
728
912
  try {
729
913
  // TODO: Add your server HTTP request here
730
- // Example:
731
- // ApiService.rejectCall(callId, reason)
732
914
  Log.d(TAG, "Server rejection request would be made here for callId: $callId, reason: $reason")
733
915
  } catch (e: Exception) {
734
916
  Log.e(TAG, "Failed to send rejection to server", e)
@@ -851,7 +1033,23 @@ object CallEngine {
851
1033
  // --- Service Management ---
852
1034
  fun startForegroundService(context: Context) {
853
1035
  Log.d(TAG, "Starting CallForegroundService.")
1036
+
1037
+ // Find the current active call to pass its info
1038
+ val currentCall = activeCalls.values.find {
1039
+ it.state == CallState.ACTIVE || it.state == CallState.INCOMING ||
1040
+ it.state == CallState.DIALING || it.state == CallState.HELD ||
1041
+ it.state == CallState.HELD_BY_SYSTEM
1042
+ }
1043
+
854
1044
  val intent = Intent(context, CallForegroundService::class.java)
1045
+
1046
+ if (currentCall != null) {
1047
+ intent.putExtra("callId", currentCall.callId)
1048
+ intent.putExtra("callData", currentCall.callData)
1049
+ intent.putExtra("state", currentCall.state.name)
1050
+ Log.d(TAG, "Starting foreground service with call info: ${currentCall.callId}")
1051
+ }
1052
+
855
1053
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
856
1054
  context.startForegroundService(intent)
857
1055
  } else {
@@ -1005,6 +1203,7 @@ object CallEngine {
1005
1203
  put("callData", callInfo.callData)
1006
1204
  put("state", newState.name)
1007
1205
  put("callType", callInfo.callType)
1206
+ put("wasHeldBySystem", callInfo.wasHeldBySystem)
1008
1207
  }
1009
1208
 
1010
1209
  Log.d(TAG, "Specific call state changed. Emitting CALL_STATE_CHANGED for $callId: $newState")
@@ -1014,8 +1213,9 @@ object CallEngine {
1014
1213
  private fun updateForegroundNotification(context: Context) {
1015
1214
  val activeCall = activeCalls.values.find { it.state == CallState.ACTIVE }
1016
1215
  val heldCall = activeCalls.values.find { it.state == CallState.HELD }
1216
+ val heldBySystemCall = activeCalls.values.find { it.state == CallState.HELD_BY_SYSTEM }
1017
1217
 
1018
- val callToShow = activeCall ?: heldCall
1218
+ val callToShow = activeCall ?: heldCall ?: heldBySystemCall
1019
1219
  callToShow?.let {
1020
1220
  val intent = Intent(context, CallForegroundService::class.java)
1021
1221
  intent.putExtra("UPDATE_NOTIFICATION", true)
@@ -1036,5 +1236,8 @@ object CallEngine {
1036
1236
  stopForegroundService(context)
1037
1237
  keepScreenAwake(context, false)
1038
1238
  resetAudioMode(context)
1239
+ abandonAudioFocus()
1240
+ isSystemCallActive = false
1241
+ wasHeldBySystem = false
1039
1242
  }
1040
1243
  }
@@ -30,20 +30,20 @@ class CallForegroundService : Service() {
30
30
  override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
31
31
  Log.d(TAG, "Service onStartCommand")
32
32
 
33
- if (intent?.getBooleanExtra("UPDATE_NOTIFICATION", false) == true) {
34
- // Update existing notification with new call data
35
- val callId = intent.getStringExtra("callId") ?: ""
36
- val callData = intent.getStringExtra("callData") ?: ""
37
- val state = intent.getStringExtra("state") ?: "ACTIVE"
38
-
39
- val notification = buildEnhancedNotification(callId, callData, state)
40
- startForeground(NOTIFICATION_ID, notification)
33
+ // Check if we have call info in the intent
34
+ val callId = intent?.getStringExtra("callId")
35
+ val callData = intent?.getStringExtra("callData")
36
+ val state = intent?.getStringExtra("state")
37
+
38
+ val notification = if (callId != null && callData != null && state != null) {
39
+ Log.d(TAG, "Building enhanced notification with call info: $callId")
40
+ buildEnhancedNotification(callId, callData, state)
41
41
  } else {
42
- // Start with basic notification, will be updated when call info is available
43
- val notification = buildBasicNotification()
44
- startForeground(NOTIFICATION_ID, notification)
42
+ Log.d(TAG, "Building basic notification - no call info available")
43
+ buildBasicNotification()
45
44
  }
46
45
 
46
+ startForeground(NOTIFICATION_ID, notification)
47
47
  return START_STICKY
48
48
  }
49
49
 
@@ -56,12 +56,12 @@ class CallForegroundService : Service() {
56
56
  Log.d(TAG, "Building basic foreground notification.")
57
57
 
58
58
  return NotificationCompat.Builder(this, CHANNEL_ID)
59
- .setContentTitle("Call in Progress")
60
- .setContentText("Managing call...")
59
+ .setContentTitle("Call Service")
60
+ .setContentText("Call service is running...")
61
61
  .setSmallIcon(android.R.drawable.sym_call_incoming)
62
62
  .setOngoing(true)
63
63
  .setCategory(NotificationCompat.CATEGORY_CALL)
64
- .setPriority(NotificationCompat.PRIORITY_HIGH)
64
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
65
65
  .setWhen(System.currentTimeMillis())
66
66
  .build()
67
67
  }
@@ -117,37 +117,58 @@ class CallForegroundService : Service() {
117
117
  }
118
118
 
119
119
  val statusText = when (state) {
120
- "ACTIVE" -> "In call with $callerName"
120
+ "ACTIVE" -> "$callerName"
121
121
  "HELD" -> "$callerName (on hold)"
122
122
  "DIALING" -> "Calling $callerName..."
123
- else -> "Call with $callerName"
123
+ "INCOMING" -> "Incoming call from $callerName"
124
+ else -> callerName
125
+ }
126
+
127
+ val titleText = when (state) {
128
+ "ACTIVE" -> "$callType Call Active"
129
+ "HELD" -> "$callType Call Held"
130
+ "DIALING" -> "Outgoing $callType Call"
131
+ "INCOMING" -> "Incoming $callType Call"
132
+ else -> "$callType Call"
124
133
  }
125
134
 
126
- val notification = NotificationCompat.Builder(this, CHANNEL_ID)
127
- .setContentTitle("$callType Call")
135
+ val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
136
+ .setContentTitle(titleText)
128
137
  .setContentText(statusText)
129
138
  .setSmallIcon(android.R.drawable.sym_call_incoming)
130
139
  .setOngoing(true)
131
140
  .setCategory(NotificationCompat.CATEGORY_CALL)
132
141
  .setPriority(NotificationCompat.PRIORITY_HIGH)
133
142
  .setWhen(System.currentTimeMillis())
134
- .addAction(
135
- android.R.drawable.ic_menu_call,
136
- holdText,
137
- holdPendingIntent
138
- )
139
- .addAction(
143
+
144
+ // Add actions for ACTIVE and HELD calls only
145
+ if (state == "ACTIVE" || state == "HELD") {
146
+ notificationBuilder
147
+ .addAction(
148
+ if (isHeld) android.R.drawable.ic_media_play else android.R.drawable.ic_media_pause,
149
+ holdText,
150
+ holdPendingIntent
151
+ )
152
+ .addAction(
153
+ android.R.drawable.sym_call_outgoing,
154
+ "End Call",
155
+ endCallPendingIntent
156
+ )
157
+ } else if (state == "DIALING") {
158
+ // For dialing calls, only show end call
159
+ notificationBuilder.addAction(
140
160
  android.R.drawable.sym_call_outgoing,
141
161
  "End Call",
142
162
  endCallPendingIntent
143
163
  )
164
+ }
144
165
 
145
166
  // Set content intent to open the main app
146
167
  mainPendingIntent?.let {
147
- notification.setContentIntent(it)
168
+ notificationBuilder.setContentIntent(it)
148
169
  }
149
170
 
150
- return notification.build()
171
+ return notificationBuilder.build()
151
172
  }
152
173
 
153
174
  private fun createNotificationChannel() {
@@ -33,7 +33,9 @@ class MyConnection(
33
33
  } catch (e: Exception) { "Audio" }
34
34
 
35
35
  connectionProperties = Connection.PROPERTY_SELF_MANAGED
36
- connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or Connection.CAPABILITY_MUTE
36
+ connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or
37
+ Connection.CAPABILITY_MUTE or
38
+ Connection.CAPABILITY_HOLD
37
39
 
38
40
  if (currentCallType == "Video") {
39
41
  Log.d(TAG, "MyConnection for callId $callId initialized as VIDEO call.")
@@ -70,12 +72,18 @@ class MyConnection(
70
72
  override fun onHold() {
71
73
  super.onHold()
72
74
  Log.d(TAG, "Call held via Telecom for callId: $callId")
75
+
76
+ // This is called by the system when it wants to hold our call
77
+ // Usually happens when a phone call comes in
73
78
  CallEngine.holdCall(context, callId)
74
79
  }
75
80
 
76
81
  override fun onUnhold() {
77
82
  super.onUnhold()
78
83
  Log.d(TAG, "Call unheld via Telecom for callId: $callId")
84
+
85
+ // This is called by the system when it's safe to resume our call
86
+ // Usually happens when a phone call ends
79
87
  CallEngine.unholdCall(context, callId)
80
88
  }
81
89
 
@@ -130,4 +138,22 @@ class MyConnection(
130
138
  Log.d(TAG, "onShowIncomingCallUi for callId: $callId")
131
139
  // Don't bring app to foreground for incoming calls automatically
132
140
  }
141
+
142
+ override fun onStateChanged(state: Int) {
143
+ super.onStateChanged(state)
144
+ Log.d(TAG, "Connection state changed for callId: $callId. New state: $state")
145
+
146
+ when (state) {
147
+ STATE_HOLDING -> {
148
+ Log.d(TAG, "Connection is now holding for callId: $callId")
149
+ }
150
+ STATE_ACTIVE -> {
151
+ Log.d(TAG, "Connection is now active for callId: $callId")
152
+ }
153
+ STATE_DISCONNECTED -> {
154
+ Log.d(TAG, "Connection is now disconnected for callId: $callId")
155
+ CallEngine.removeTelecomConnection(callId)
156
+ }
157
+ }
158
+ }
133
159
  }
@@ -1,2 +1,2 @@
1
- export type CallEventType = 'INITIAL_CALL_STATE' | 'CALL_STATE_CHANGED' | 'AUDIO_DEVICES_CHANGED' | 'AUDIO_ROUTE_CHANGED' | 'CALL_HELD' | 'CALL_UNHELD' | 'CALL_MUTED' | 'CALL_UNMUTED' | 'CALL_ANSWERED' | 'CALL_REJECTED' | 'CALL_ENDED' | 'DTMF_TONE';
1
+ export type CallEventType = 'INITIAL_CALL_STATE' | 'CALL_STATE_CHANGED' | 'AUDIO_DEVICES_CHANGED' | 'AUDIO_ROUTE_CHANGED' | 'CALL_HELD' | 'CALL_UNHELD' | 'CALL_UNHOLD_FAILED' | 'CALL_MUTED' | 'CALL_UNMUTED' | 'CALL_ANSWERED' | 'CALL_REJECTED' | 'CALL_ENDED' | 'DTMF_TONE';
2
2
  //# sourceMappingURL=CallEventType.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CallEventType.d.ts","sourceRoot":"","sources":["../../../src/CallEventType.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,aAAa,GACrB,oBAAoB,GACpB,oBAAoB,GACpB,uBAAuB,GACvB,qBAAqB,GACrB,WAAW,GACX,aAAa,GACb,YAAY,GACZ,cAAc,GACd,eAAe,GACf,eAAe,GACf,YAAY,GACZ,WAAW,CAAC"}
1
+ {"version":3,"file":"CallEventType.d.ts","sourceRoot":"","sources":["../../../src/CallEventType.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,aAAa,GACrB,oBAAoB,GACpB,oBAAoB,GACpB,uBAAuB,GACvB,qBAAqB,GACrB,WAAW,GACX,aAAa,GACb,oBAAoB,GACpB,YAAY,GACZ,cAAc,GACd,eAAe,GACf,eAAe,GACf,YAAY,GACZ,WAAW,CAAC"}
@@ -47,6 +47,7 @@ namespace margelo::nitro::qusaieilouti99_callmanager {
47
47
  static const auto fieldAUDIO_ROUTE_CHANGED = clazz->getStaticField<JCallEventType>("AUDIO_ROUTE_CHANGED");
48
48
  static const auto fieldCALL_HELD = clazz->getStaticField<JCallEventType>("CALL_HELD");
49
49
  static const auto fieldCALL_UNHELD = clazz->getStaticField<JCallEventType>("CALL_UNHELD");
50
+ static const auto fieldCALL_UNHOLD_FAILED = clazz->getStaticField<JCallEventType>("CALL_UNHOLD_FAILED");
50
51
  static const auto fieldCALL_MUTED = clazz->getStaticField<JCallEventType>("CALL_MUTED");
51
52
  static const auto fieldCALL_UNMUTED = clazz->getStaticField<JCallEventType>("CALL_UNMUTED");
52
53
  static const auto fieldCALL_ANSWERED = clazz->getStaticField<JCallEventType>("CALL_ANSWERED");
@@ -67,6 +68,8 @@ namespace margelo::nitro::qusaieilouti99_callmanager {
67
68
  return clazz->getStaticFieldValue(fieldCALL_HELD);
68
69
  case CallEventType::CALL_UNHELD:
69
70
  return clazz->getStaticFieldValue(fieldCALL_UNHELD);
71
+ case CallEventType::CALL_UNHOLD_FAILED:
72
+ return clazz->getStaticFieldValue(fieldCALL_UNHOLD_FAILED);
70
73
  case CallEventType::CALL_MUTED:
71
74
  return clazz->getStaticFieldValue(fieldCALL_MUTED);
72
75
  case CallEventType::CALL_UNMUTED:
@@ -22,6 +22,7 @@ enum class CallEventType {
22
22
  AUDIO_ROUTE_CHANGED,
23
23
  CALL_HELD,
24
24
  CALL_UNHELD,
25
+ CALL_UNHOLD_FAILED,
25
26
  CALL_MUTED,
26
27
  CALL_UNMUTED,
27
28
  CALL_ANSWERED,
@@ -29,6 +29,8 @@ public extension CallEventType {
29
29
  self = .callHeld
30
30
  case "CALL_UNHELD":
31
31
  self = .callUnheld
32
+ case "CALL_UNHOLD_FAILED":
33
+ self = .callUnholdFailed
32
34
  case "CALL_MUTED":
33
35
  self = .callMuted
34
36
  case "CALL_UNMUTED":
@@ -63,6 +65,8 @@ public extension CallEventType {
63
65
  return "CALL_HELD"
64
66
  case .callUnheld:
65
67
  return "CALL_UNHELD"
68
+ case .callUnholdFailed:
69
+ return "CALL_UNHOLD_FAILED"
66
70
  case .callMuted:
67
71
  return "CALL_MUTED"
68
72
  case .callUnmuted:
@@ -35,12 +35,13 @@ namespace margelo::nitro::qusaieilouti99_callmanager {
35
35
  AUDIO_ROUTE_CHANGED SWIFT_NAME(audioRouteChanged) = 3,
36
36
  CALL_HELD SWIFT_NAME(callHeld) = 4,
37
37
  CALL_UNHELD SWIFT_NAME(callUnheld) = 5,
38
- CALL_MUTED SWIFT_NAME(callMuted) = 6,
39
- CALL_UNMUTED SWIFT_NAME(callUnmuted) = 7,
40
- CALL_ANSWERED SWIFT_NAME(callAnswered) = 8,
41
- CALL_REJECTED SWIFT_NAME(callRejected) = 9,
42
- CALL_ENDED SWIFT_NAME(callEnded) = 10,
43
- DTMF_TONE SWIFT_NAME(dtmfTone) = 11,
38
+ CALL_UNHOLD_FAILED SWIFT_NAME(callUnholdFailed) = 6,
39
+ CALL_MUTED SWIFT_NAME(callMuted) = 7,
40
+ CALL_UNMUTED SWIFT_NAME(callUnmuted) = 8,
41
+ CALL_ANSWERED SWIFT_NAME(callAnswered) = 9,
42
+ CALL_REJECTED SWIFT_NAME(callRejected) = 10,
43
+ CALL_ENDED SWIFT_NAME(callEnded) = 11,
44
+ DTMF_TONE SWIFT_NAME(dtmfTone) = 12,
44
45
  } CLOSED_ENUM;
45
46
 
46
47
  } // namespace margelo::nitro::qusaieilouti99_callmanager
@@ -61,6 +62,7 @@ namespace margelo::nitro {
61
62
  case hashString("AUDIO_ROUTE_CHANGED"): return CallEventType::AUDIO_ROUTE_CHANGED;
62
63
  case hashString("CALL_HELD"): return CallEventType::CALL_HELD;
63
64
  case hashString("CALL_UNHELD"): return CallEventType::CALL_UNHELD;
65
+ case hashString("CALL_UNHOLD_FAILED"): return CallEventType::CALL_UNHOLD_FAILED;
64
66
  case hashString("CALL_MUTED"): return CallEventType::CALL_MUTED;
65
67
  case hashString("CALL_UNMUTED"): return CallEventType::CALL_UNMUTED;
66
68
  case hashString("CALL_ANSWERED"): return CallEventType::CALL_ANSWERED;
@@ -79,6 +81,7 @@ namespace margelo::nitro {
79
81
  case CallEventType::AUDIO_ROUTE_CHANGED: return JSIConverter<std::string>::toJSI(runtime, "AUDIO_ROUTE_CHANGED");
80
82
  case CallEventType::CALL_HELD: return JSIConverter<std::string>::toJSI(runtime, "CALL_HELD");
81
83
  case CallEventType::CALL_UNHELD: return JSIConverter<std::string>::toJSI(runtime, "CALL_UNHELD");
84
+ case CallEventType::CALL_UNHOLD_FAILED: return JSIConverter<std::string>::toJSI(runtime, "CALL_UNHOLD_FAILED");
82
85
  case CallEventType::CALL_MUTED: return JSIConverter<std::string>::toJSI(runtime, "CALL_MUTED");
83
86
  case CallEventType::CALL_UNMUTED: return JSIConverter<std::string>::toJSI(runtime, "CALL_UNMUTED");
84
87
  case CallEventType::CALL_ANSWERED: return JSIConverter<std::string>::toJSI(runtime, "CALL_ANSWERED");
@@ -102,6 +105,7 @@ namespace margelo::nitro {
102
105
  case hashString("AUDIO_ROUTE_CHANGED"):
103
106
  case hashString("CALL_HELD"):
104
107
  case hashString("CALL_UNHELD"):
108
+ case hashString("CALL_UNHOLD_FAILED"):
105
109
  case hashString("CALL_MUTED"):
106
110
  case hashString("CALL_UNMUTED"):
107
111
  case hashString("CALL_ANSWERED"):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -6,6 +6,7 @@ export type CallEventType =
6
6
  | 'AUDIO_ROUTE_CHANGED'
7
7
  | 'CALL_HELD'
8
8
  | 'CALL_UNHELD'
9
+ | 'CALL_UNHOLD_FAILED'
9
10
  | 'CALL_MUTED'
10
11
  | 'CALL_UNMUTED'
11
12
  | 'CALL_ANSWERED'