@qusaieilouti99/call-manager 0.1.62 → 0.1.64
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.
|
@@ -48,8 +48,10 @@ object CallEngine {
|
|
|
48
48
|
private const val FOREGROUND_NOTIF_ID = 1001
|
|
49
49
|
|
|
50
50
|
// Core context - initialized once and maintained
|
|
51
|
+
@Volatile
|
|
51
52
|
private var appContext: Context? = null
|
|
52
|
-
private
|
|
53
|
+
private val isInitialized = AtomicBoolean(false)
|
|
54
|
+
private val initializationLock = Any()
|
|
53
55
|
|
|
54
56
|
// Audio & Media
|
|
55
57
|
private var ringtone: android.media.Ringtone? = null
|
|
@@ -83,16 +85,25 @@ object CallEngine {
|
|
|
83
85
|
fun onLockScreenBypassChanged(shouldBypass: Boolean)
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
// --- INITIALIZATION -
|
|
88
|
+
// --- INITIALIZATION - Fixed for better context management ---
|
|
87
89
|
fun initialize(context: Context) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
synchronized(initializationLock) {
|
|
91
|
+
if (isInitialized.compareAndSet(false, true)) {
|
|
92
|
+
appContext = context.applicationContext
|
|
93
|
+
audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
94
|
+
Log.d(TAG, "CallEngine initialized successfully with context: ${context.javaClass.simpleName}")
|
|
95
|
+
|
|
96
|
+
// Verify critical services are available
|
|
97
|
+
if (audioManager == null) {
|
|
98
|
+
Log.w(TAG, "AudioManager is null after initialization")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Initialize foreground service if needed
|
|
102
|
+
if (isCallActive()) {
|
|
103
|
+
startForegroundService()
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
Log.d(TAG, "CallEngine already initialized, skipping")
|
|
96
107
|
}
|
|
97
108
|
}
|
|
98
109
|
}
|
|
@@ -100,7 +111,9 @@ object CallEngine {
|
|
|
100
111
|
fun isInitialized(): Boolean = isInitialized.get()
|
|
101
112
|
|
|
102
113
|
private fun requireContext(): Context {
|
|
103
|
-
return appContext ?: throw IllegalStateException(
|
|
114
|
+
return appContext ?: throw IllegalStateException(
|
|
115
|
+
"CallEngine not initialized. Ensure CallEngine.initialize(context) is called in Application.onCreate() before any module usage."
|
|
116
|
+
)
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
// --- Event System ---
|
|
@@ -128,38 +141,79 @@ object CallEngine {
|
|
|
128
141
|
}
|
|
129
142
|
}
|
|
130
143
|
|
|
131
|
-
// --- Audio Focus Management
|
|
144
|
+
// --- FIXED Audio Focus Management ---
|
|
132
145
|
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
|
133
146
|
Log.d(TAG, "Audio focus changed: $focusChange")
|
|
134
147
|
when (focusChange) {
|
|
135
|
-
AudioManager.AUDIOFOCUS_LOSS
|
|
148
|
+
AudioManager.AUDIOFOCUS_LOSS -> {
|
|
149
|
+
Log.d(TAG, "Permanent audio focus loss - another app took focus")
|
|
150
|
+
hasAudioFocus = false
|
|
151
|
+
isSystemCallActive = true
|
|
152
|
+
holdSystemCalls()
|
|
153
|
+
}
|
|
136
154
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
155
|
+
Log.d(TAG, "Transient audio focus loss - temporary interruption")
|
|
137
156
|
hasAudioFocus = false
|
|
138
157
|
isSystemCallActive = true
|
|
139
158
|
holdSystemCalls()
|
|
140
159
|
}
|
|
160
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
|
161
|
+
Log.d(TAG, "Audio focus loss with ducking - lowering volume but keeping active")
|
|
162
|
+
// Don't hold the call for ducking, just lower volume
|
|
163
|
+
hasAudioFocus = false
|
|
164
|
+
}
|
|
141
165
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
166
|
+
Log.d(TAG, "Audio focus gained")
|
|
142
167
|
hasAudioFocus = true
|
|
143
168
|
isSystemCallActive = false
|
|
169
|
+
// Delay resuming to avoid rapid hold/unhold cycles
|
|
144
170
|
Handler(Looper.getMainLooper()).postDelayed({
|
|
145
171
|
resumeSystemHeldCalls()
|
|
146
|
-
},
|
|
172
|
+
}, 500) // Reduced from 1000ms
|
|
173
|
+
}
|
|
174
|
+
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> {
|
|
175
|
+
Log.d(TAG, "Transient audio focus gained")
|
|
176
|
+
hasAudioFocus = true
|
|
147
177
|
}
|
|
148
178
|
}
|
|
149
179
|
updateForegroundNotification()
|
|
150
180
|
}
|
|
151
181
|
|
|
152
182
|
private fun holdSystemCalls() {
|
|
153
|
-
activeCalls.values.filter {
|
|
154
|
-
|
|
183
|
+
val callsToHold = activeCalls.values.filter {
|
|
184
|
+
it.state == CallState.ACTIVE && !it.wasHeldBySystem
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (callsToHold.isEmpty()) {
|
|
188
|
+
Log.d(TAG, "No active calls to hold due to audio focus loss")
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
Log.d(TAG, "Holding ${callsToHold.size} calls due to audio focus loss")
|
|
193
|
+
callsToHold.forEach { call ->
|
|
194
|
+
// Add a small delay to prevent holding immediately after answering
|
|
195
|
+
val timeSinceAnswer = System.currentTimeMillis() - call.timestamp
|
|
196
|
+
if (timeSinceAnswer > 2000) { // Only hold if call has been active for 2+ seconds
|
|
155
197
|
holdCallInternal(call.callId, heldBySystem = true)
|
|
198
|
+
} else {
|
|
199
|
+
Log.d(TAG, "Skipping hold for recently answered call: ${call.callId}")
|
|
156
200
|
}
|
|
157
201
|
}
|
|
158
202
|
stopRingback()
|
|
159
203
|
}
|
|
160
204
|
|
|
161
205
|
private fun resumeSystemHeldCalls() {
|
|
162
|
-
activeCalls.values.filter {
|
|
206
|
+
val callsToResume = activeCalls.values.filter {
|
|
207
|
+
it.state == CallState.HELD && it.wasHeldBySystem
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (callsToResume.isEmpty()) {
|
|
211
|
+
Log.d(TAG, "No system-held calls to resume")
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
Log.d(TAG, "Resuming ${callsToResume.size} system-held calls")
|
|
216
|
+
callsToResume.forEach { call ->
|
|
163
217
|
unholdCallInternal(call.callId, resumedBySystem = true)
|
|
164
218
|
}
|
|
165
219
|
}
|
|
@@ -177,12 +231,13 @@ object CallEngine {
|
|
|
177
231
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
178
232
|
.build()
|
|
179
233
|
)
|
|
180
|
-
.setOnAudioFocusChangeListener(audioFocusChangeListener)
|
|
234
|
+
.setOnAudioFocusChangeListener(audioFocusChangeListener, Handler(Looper.getMainLooper()))
|
|
235
|
+
.setAcceptsDelayedFocusGain(true) // Added this
|
|
181
236
|
.build()
|
|
182
237
|
}
|
|
183
238
|
val result = audioManager?.requestAudioFocus(audioFocusRequest!!)
|
|
184
239
|
hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
185
|
-
Log.d(TAG, "Audio focus request result: $result")
|
|
240
|
+
Log.d(TAG, "Audio focus request result: $result (granted: $hasAudioFocus)")
|
|
186
241
|
hasAudioFocus
|
|
187
242
|
} else {
|
|
188
243
|
@Suppress("DEPRECATION")
|
|
@@ -192,7 +247,7 @@ object CallEngine {
|
|
|
192
247
|
AudioManager.AUDIOFOCUS_GAIN
|
|
193
248
|
)
|
|
194
249
|
hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
195
|
-
Log.d(TAG, "Audio focus request result (legacy): $result")
|
|
250
|
+
Log.d(TAG, "Audio focus request result (legacy): $result (granted: $hasAudioFocus)")
|
|
196
251
|
hasAudioFocus
|
|
197
252
|
}
|
|
198
253
|
}
|
|
@@ -330,7 +385,7 @@ object CallEngine {
|
|
|
330
385
|
|
|
331
386
|
try {
|
|
332
387
|
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
|
|
333
|
-
startForegroundService()
|
|
388
|
+
startForegroundService()
|
|
334
389
|
Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
|
|
335
390
|
} catch (e: SecurityException) {
|
|
336
391
|
Log.e(TAG, "SecurityException: Failed to report incoming call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
|
|
@@ -380,7 +435,7 @@ object CallEngine {
|
|
|
380
435
|
val phoneAccountHandle = getPhoneAccountHandle()
|
|
381
436
|
val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
|
|
382
437
|
|
|
383
|
-
//
|
|
438
|
+
// Build a bundle of ONLY your own keys
|
|
384
439
|
val outgoingExtras = Bundle().apply {
|
|
385
440
|
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
386
441
|
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
@@ -389,7 +444,7 @@ object CallEngine {
|
|
|
389
444
|
metadata?.let { putString("metadata", it) }
|
|
390
445
|
}
|
|
391
446
|
|
|
392
|
-
//
|
|
447
|
+
// Wrap under the single Telecom-honored key
|
|
393
448
|
val extras = Bundle().apply {
|
|
394
449
|
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
|
|
395
450
|
putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
|
|
@@ -399,8 +454,15 @@ object CallEngine {
|
|
|
399
454
|
try {
|
|
400
455
|
telecomManager.placeCall(addressUri, extras)
|
|
401
456
|
startForegroundService()
|
|
402
|
-
|
|
403
|
-
|
|
457
|
+
|
|
458
|
+
// FIXED: Request audio focus BEFORE starting ringback
|
|
459
|
+
val audioFocusGranted = requestAudioFocus()
|
|
460
|
+
if (audioFocusGranted) {
|
|
461
|
+
startRingback()
|
|
462
|
+
} else {
|
|
463
|
+
Log.w(TAG, "Audio focus not granted for outgoing call, skipping ringback")
|
|
464
|
+
}
|
|
465
|
+
|
|
404
466
|
bringAppToForeground()
|
|
405
467
|
keepScreenAwake(true)
|
|
406
468
|
setInitialAudioRoute(callType)
|
|
@@ -449,13 +511,13 @@ object CallEngine {
|
|
|
449
511
|
registerPhoneAccount()
|
|
450
512
|
requestAudioFocus()
|
|
451
513
|
bringAppToForeground()
|
|
452
|
-
startForegroundService()
|
|
514
|
+
startForegroundService()
|
|
453
515
|
keepScreenAwake(true)
|
|
454
516
|
setInitialAudioRoute(callType)
|
|
455
517
|
updateLockScreenBypass()
|
|
456
518
|
|
|
457
|
-
// Emit call answered event with metadata
|
|
458
|
-
|
|
519
|
+
// Emit outgoing call answered event with metadata for JS-initiated calls
|
|
520
|
+
emitOutgoingCallAnsweredWithMetadata(callId)
|
|
459
521
|
}
|
|
460
522
|
|
|
461
523
|
// --- Call Answer Management ---
|
|
@@ -469,6 +531,7 @@ object CallEngine {
|
|
|
469
531
|
coreCallAnswered(callId, isLocalAnswer = true)
|
|
470
532
|
}
|
|
471
533
|
|
|
534
|
+
// FIXED: Core Call Answered Method with proper audio focus handling
|
|
472
535
|
private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
|
|
473
536
|
val context = requireContext()
|
|
474
537
|
Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
|
|
@@ -479,13 +542,27 @@ object CallEngine {
|
|
|
479
542
|
return
|
|
480
543
|
}
|
|
481
544
|
|
|
545
|
+
// FIXED: Request audio focus FIRST, before stopping media
|
|
546
|
+
val audioFocusGranted = requestAudioFocus()
|
|
547
|
+
if (!audioFocusGranted) {
|
|
548
|
+
Log.w(TAG, "Failed to get audio focus for call $callId, but continuing...")
|
|
549
|
+
// Don't fail the call, but warn about audio issues
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Stop media AFTER getting audio focus
|
|
482
553
|
stopRingtone()
|
|
483
554
|
stopRingback()
|
|
484
555
|
cancelIncomingCallUI()
|
|
485
|
-
requestAudioFocus()
|
|
486
556
|
|
|
487
|
-
|
|
488
|
-
|
|
557
|
+
// FIXED: Only set call to ACTIVE if we have audio focus OR if it's a remote answer
|
|
558
|
+
if (audioFocusGranted || !isLocalAnswer) {
|
|
559
|
+
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
560
|
+
currentCallId = callId
|
|
561
|
+
Log.d(TAG, "Call $callId set to ACTIVE state")
|
|
562
|
+
} else {
|
|
563
|
+
Log.w(TAG, "Call $callId not set to ACTIVE due to audio focus failure")
|
|
564
|
+
return
|
|
565
|
+
}
|
|
489
566
|
|
|
490
567
|
if (!canMakeMultipleCalls) {
|
|
491
568
|
activeCalls.filter { it.key != callId }.values.forEach { otherCall ->
|
|
@@ -496,18 +573,25 @@ object CallEngine {
|
|
|
496
573
|
}
|
|
497
574
|
|
|
498
575
|
bringAppToForeground()
|
|
499
|
-
startForegroundService()
|
|
576
|
+
startForegroundService()
|
|
500
577
|
keepScreenAwake(true)
|
|
501
578
|
resetAudioMode()
|
|
502
579
|
updateLockScreenBypass()
|
|
503
580
|
updateForegroundNotification()
|
|
504
581
|
|
|
505
|
-
// Emit
|
|
506
|
-
|
|
582
|
+
// FIXED: Emit different events based on call direction
|
|
583
|
+
if (isLocalAnswer) {
|
|
584
|
+
// This is for incoming calls - user answered locally
|
|
585
|
+
emitCallAnsweredWithMetadata(callId)
|
|
586
|
+
} else {
|
|
587
|
+
// This is for outgoing calls - remote party answered
|
|
588
|
+
emitOutgoingCallAnsweredWithMetadata(callId)
|
|
589
|
+
}
|
|
507
590
|
|
|
508
591
|
Log.d(TAG, "Call $callId successfully answered and UI cleaned up")
|
|
509
592
|
}
|
|
510
593
|
|
|
594
|
+
// For incoming calls (local answer)
|
|
511
595
|
private fun emitCallAnsweredWithMetadata(callId: String) {
|
|
512
596
|
val callInfo = activeCalls[callId] ?: return
|
|
513
597
|
val metadata = callMetadata[callId]
|
|
@@ -528,6 +612,27 @@ object CallEngine {
|
|
|
528
612
|
})
|
|
529
613
|
}
|
|
530
614
|
|
|
615
|
+
// For outgoing calls (remote answer)
|
|
616
|
+
private fun emitOutgoingCallAnsweredWithMetadata(callId: String) {
|
|
617
|
+
val callInfo = activeCalls[callId] ?: return
|
|
618
|
+
val metadata = callMetadata[callId]
|
|
619
|
+
|
|
620
|
+
emitEvent(CallEventType.OUTGOING_CALL_ANSWERED, JSONObject().apply {
|
|
621
|
+
put("callId", callId)
|
|
622
|
+
put("callType", callInfo.callType)
|
|
623
|
+
put("displayName", callInfo.displayName)
|
|
624
|
+
callInfo.pictureUrl?.let { put("pictureUrl", it) }
|
|
625
|
+
metadata?.let {
|
|
626
|
+
try {
|
|
627
|
+
put("metadata", JSONObject(it))
|
|
628
|
+
} catch (e: Exception) {
|
|
629
|
+
Log.w(TAG, "Invalid metadata JSON for callId: $callId", e)
|
|
630
|
+
put("metadata", it) // fallback to string
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
})
|
|
634
|
+
}
|
|
635
|
+
|
|
531
636
|
// --- Call Control Methods ---
|
|
532
637
|
fun holdCall(callId: String) {
|
|
533
638
|
holdCallInternal(callId, heldBySystem = false)
|
|
@@ -586,7 +691,7 @@ object CallEngine {
|
|
|
586
691
|
return
|
|
587
692
|
}
|
|
588
693
|
|
|
589
|
-
//
|
|
694
|
+
// FIXED: Simplified audio focus check to prevent UNHELD FAILED
|
|
590
695
|
if (!hasAudioFocus && !resumedBySystem && !requestAudioFocus()) {
|
|
591
696
|
Log.w(TAG, "Failed to get audio focus for unhold - but continuing anyway")
|
|
592
697
|
// Don't emit UNHELD FAILED - just continue
|
|
@@ -1,48 +1,26 @@
|
|
|
1
1
|
package com.margelo.nitro.qusaieilouti99.callmanager
|
|
2
2
|
|
|
3
3
|
import android.util.Log
|
|
4
|
-
import com.facebook.react.bridge.ReactApplicationContext
|
|
5
4
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
6
5
|
|
|
7
6
|
@DoNotStrip
|
|
8
|
-
class CallManager : HybridCallManagerSpec() {
|
|
7
|
+
class CallManager : HybridCallManagerSpec() {
|
|
9
8
|
|
|
10
9
|
private val TAG = "CallManager"
|
|
11
10
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// Simplified approach - rely on proper Application.onCreate() initialization
|
|
12
|
+
// Remove all fallback context access attempts that don't work with Nitro modules
|
|
14
13
|
private fun ensureInitialized() {
|
|
15
14
|
if (!CallEngine.isInitialized()) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// ReactApplicationContext.getCurrentApplicationContext() provides the
|
|
22
|
-
// ReactApplicationContext once the bridge is set up. From there, we get the Application context.
|
|
23
|
-
val reactContext = ReactApplicationContext.getCurrentApplicationContext()
|
|
24
|
-
|
|
25
|
-
if (reactContext != null) {
|
|
26
|
-
val appContext = reactContext.applicationContext
|
|
27
|
-
if (appContext != null) {
|
|
28
|
-
CallEngine.initialize(appContext)
|
|
29
|
-
Log.d(TAG, "CallEngine successfully (late) initialized via ReactApplicationContext.")
|
|
30
|
-
} else {
|
|
31
|
-
Log.e(TAG, "Error: reactContext.applicationContext is null. Cannot late-initialize CallEngine.")
|
|
32
|
-
throw IllegalStateException("CallEngine initialization failed: Application context is null.")
|
|
33
|
-
}
|
|
34
|
-
} else {
|
|
35
|
-
Log.e(TAG, "Error: ReactApplicationContext.getCurrentApplicationContext() returned null. Cannot late-initialize CallEngine.")
|
|
36
|
-
throw IllegalStateException("CallEngine initialization failed: ReactApplicationContext not available.")
|
|
37
|
-
}
|
|
38
|
-
} catch (e: Exception) {
|
|
39
|
-
Log.e(TAG, "Exception during CallEngine late initialization: ${e.message}", e)
|
|
40
|
-
throw IllegalStateException("CallEngine fatal error during initialization: ${e.message}", e)
|
|
41
|
-
}
|
|
15
|
+
Log.e(TAG, "CallEngine not initialized! This should not happen if Application.onCreate() was called properly.")
|
|
16
|
+
throw IllegalStateException(
|
|
17
|
+
"CallEngine must be initialized in Application.onCreate(). " +
|
|
18
|
+
"Make sure MainApplication.onCreate() calls CallEngine.initialize(this) before any native calls."
|
|
19
|
+
)
|
|
42
20
|
}
|
|
43
21
|
}
|
|
44
22
|
|
|
45
|
-
// --- All
|
|
23
|
+
// --- All methods must call ensureInitialized() first ---
|
|
46
24
|
|
|
47
25
|
override fun endCall(callId: String): Unit {
|
|
48
26
|
Log.d(TAG, "endCall requested for callId: $callId")
|
|
@@ -81,7 +59,7 @@ class CallManager : HybridCallManagerSpec() { // Match the parameterless constru
|
|
|
81
59
|
}
|
|
82
60
|
|
|
83
61
|
override fun addListener(listener: (event: CallEventType, payload: String) -> Unit): () -> Unit {
|
|
84
|
-
Log.d(TAG, "addListener called
|
|
62
|
+
Log.d(TAG, "addListener called")
|
|
85
63
|
ensureInitialized()
|
|
86
64
|
CallEngine.setEventHandler(listener)
|
|
87
65
|
return {
|
|
@@ -93,7 +71,6 @@ class CallManager : HybridCallManagerSpec() { // Match the parameterless constru
|
|
|
93
71
|
override fun startOutgoingCall(callId: String, callType: String, targetName: String, metadata: String?): Unit {
|
|
94
72
|
Log.d(TAG, "startOutgoingCall requested: callId=$callId, callType=$callType, targetName=$targetName")
|
|
95
73
|
ensureInitialized()
|
|
96
|
-
// IMPORTANT: The Bundle wrapping fix for TelecomManager must still be in CallEngine.startOutgoingCall
|
|
97
74
|
CallEngine.startOutgoingCall(callId, callType, targetName, metadata)
|
|
98
75
|
}
|
|
99
76
|
|