@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.
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/CallEngine.kt +223 -20
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/CallForegroundService.kt +47 -26
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/MyConnection.kt +27 -1
- package/lib/typescript/src/CallEventType.d.ts +1 -1
- package/lib/typescript/src/CallEventType.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JCallEventType.hpp +3 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/qusaieilouti99/callmanager/CallEventType.kt +1 -0
- package/nitrogen/generated/ios/swift/CallEventType.swift +4 -0
- package/nitrogen/generated/shared/c++/CallEventType.hpp +10 -6
- package/package.json +1 -1
- package/src/CallEventType.ts +1 -0
|
@@ -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
|
-
|
|
95
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
}
|
package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/CallForegroundService.kt
CHANGED
|
@@ -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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
60
|
-
.setContentText("
|
|
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.
|
|
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" -> "
|
|
120
|
+
"ACTIVE" -> "$callerName"
|
|
121
121
|
"HELD" -> "$callerName (on hold)"
|
|
122
122
|
"DIALING" -> "Calling $callerName..."
|
|
123
|
-
|
|
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
|
|
127
|
-
.setContentTitle(
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
168
|
+
notificationBuilder.setContentIntent(it)
|
|
148
169
|
}
|
|
149
170
|
|
|
150
|
-
return
|
|
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
|
|
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:
|
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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