@qusaieilouti99/call-manager 0.1.63 → 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,13 +48,11 @@ object CallEngine {
|
|
|
48
48
|
private const val FOREGROUND_NOTIF_ID = 1001
|
|
49
49
|
|
|
50
50
|
// Core context - initialized once and maintained
|
|
51
|
-
// Improved context management with thread safety
|
|
52
51
|
@Volatile
|
|
53
52
|
private var appContext: Context? = null
|
|
54
53
|
private val isInitialized = AtomicBoolean(false)
|
|
55
54
|
private val initializationLock = Any()
|
|
56
55
|
|
|
57
|
-
|
|
58
56
|
// Audio & Media
|
|
59
57
|
private var ringtone: android.media.Ringtone? = null
|
|
60
58
|
private var ringbackPlayer: MediaPlayer? = null
|
|
@@ -87,7 +85,7 @@ object CallEngine {
|
|
|
87
85
|
fun onLockScreenBypassChanged(shouldBypass: Boolean)
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
// --- INITIALIZATION
|
|
88
|
+
// --- INITIALIZATION - Fixed for better context management ---
|
|
91
89
|
fun initialize(context: Context) {
|
|
92
90
|
synchronized(initializationLock) {
|
|
93
91
|
if (isInitialized.compareAndSet(false, true)) {
|
|
@@ -143,38 +141,79 @@ object CallEngine {
|
|
|
143
141
|
}
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
// --- Audio Focus Management
|
|
144
|
+
// --- FIXED Audio Focus Management ---
|
|
147
145
|
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
|
148
146
|
Log.d(TAG, "Audio focus changed: $focusChange")
|
|
149
147
|
when (focusChange) {
|
|
150
|
-
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
|
+
}
|
|
151
154
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
155
|
+
Log.d(TAG, "Transient audio focus loss - temporary interruption")
|
|
152
156
|
hasAudioFocus = false
|
|
153
157
|
isSystemCallActive = true
|
|
154
158
|
holdSystemCalls()
|
|
155
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
|
+
}
|
|
156
165
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
166
|
+
Log.d(TAG, "Audio focus gained")
|
|
157
167
|
hasAudioFocus = true
|
|
158
168
|
isSystemCallActive = false
|
|
169
|
+
// Delay resuming to avoid rapid hold/unhold cycles
|
|
159
170
|
Handler(Looper.getMainLooper()).postDelayed({
|
|
160
171
|
resumeSystemHeldCalls()
|
|
161
|
-
},
|
|
172
|
+
}, 500) // Reduced from 1000ms
|
|
173
|
+
}
|
|
174
|
+
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> {
|
|
175
|
+
Log.d(TAG, "Transient audio focus gained")
|
|
176
|
+
hasAudioFocus = true
|
|
162
177
|
}
|
|
163
178
|
}
|
|
164
179
|
updateForegroundNotification()
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
private fun holdSystemCalls() {
|
|
168
|
-
activeCalls.values.filter {
|
|
169
|
-
|
|
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
|
|
170
197
|
holdCallInternal(call.callId, heldBySystem = true)
|
|
198
|
+
} else {
|
|
199
|
+
Log.d(TAG, "Skipping hold for recently answered call: ${call.callId}")
|
|
171
200
|
}
|
|
172
201
|
}
|
|
173
202
|
stopRingback()
|
|
174
203
|
}
|
|
175
204
|
|
|
176
205
|
private fun resumeSystemHeldCalls() {
|
|
177
|
-
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 ->
|
|
178
217
|
unholdCallInternal(call.callId, resumedBySystem = true)
|
|
179
218
|
}
|
|
180
219
|
}
|
|
@@ -192,12 +231,13 @@ object CallEngine {
|
|
|
192
231
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
193
232
|
.build()
|
|
194
233
|
)
|
|
195
|
-
.setOnAudioFocusChangeListener(audioFocusChangeListener)
|
|
234
|
+
.setOnAudioFocusChangeListener(audioFocusChangeListener, Handler(Looper.getMainLooper()))
|
|
235
|
+
.setAcceptsDelayedFocusGain(true) // Added this
|
|
196
236
|
.build()
|
|
197
237
|
}
|
|
198
238
|
val result = audioManager?.requestAudioFocus(audioFocusRequest!!)
|
|
199
239
|
hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
200
|
-
Log.d(TAG, "Audio focus request result: $result")
|
|
240
|
+
Log.d(TAG, "Audio focus request result: $result (granted: $hasAudioFocus)")
|
|
201
241
|
hasAudioFocus
|
|
202
242
|
} else {
|
|
203
243
|
@Suppress("DEPRECATION")
|
|
@@ -207,7 +247,7 @@ object CallEngine {
|
|
|
207
247
|
AudioManager.AUDIOFOCUS_GAIN
|
|
208
248
|
)
|
|
209
249
|
hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
210
|
-
Log.d(TAG, "Audio focus request result (legacy): $result")
|
|
250
|
+
Log.d(TAG, "Audio focus request result (legacy): $result (granted: $hasAudioFocus)")
|
|
211
251
|
hasAudioFocus
|
|
212
252
|
}
|
|
213
253
|
}
|
|
@@ -345,7 +385,7 @@ object CallEngine {
|
|
|
345
385
|
|
|
346
386
|
try {
|
|
347
387
|
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
|
|
348
|
-
startForegroundService()
|
|
388
|
+
startForegroundService()
|
|
349
389
|
Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
|
|
350
390
|
} catch (e: SecurityException) {
|
|
351
391
|
Log.e(TAG, "SecurityException: Failed to report incoming call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
|
|
@@ -395,7 +435,7 @@ object CallEngine {
|
|
|
395
435
|
val phoneAccountHandle = getPhoneAccountHandle()
|
|
396
436
|
val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
|
|
397
437
|
|
|
398
|
-
//
|
|
438
|
+
// Build a bundle of ONLY your own keys
|
|
399
439
|
val outgoingExtras = Bundle().apply {
|
|
400
440
|
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
401
441
|
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
@@ -404,7 +444,7 @@ object CallEngine {
|
|
|
404
444
|
metadata?.let { putString("metadata", it) }
|
|
405
445
|
}
|
|
406
446
|
|
|
407
|
-
//
|
|
447
|
+
// Wrap under the single Telecom-honored key
|
|
408
448
|
val extras = Bundle().apply {
|
|
409
449
|
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
|
|
410
450
|
putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
|
|
@@ -414,8 +454,15 @@ object CallEngine {
|
|
|
414
454
|
try {
|
|
415
455
|
telecomManager.placeCall(addressUri, extras)
|
|
416
456
|
startForegroundService()
|
|
417
|
-
|
|
418
|
-
|
|
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
|
+
|
|
419
466
|
bringAppToForeground()
|
|
420
467
|
keepScreenAwake(true)
|
|
421
468
|
setInitialAudioRoute(callType)
|
|
@@ -464,13 +511,13 @@ object CallEngine {
|
|
|
464
511
|
registerPhoneAccount()
|
|
465
512
|
requestAudioFocus()
|
|
466
513
|
bringAppToForeground()
|
|
467
|
-
startForegroundService()
|
|
514
|
+
startForegroundService()
|
|
468
515
|
keepScreenAwake(true)
|
|
469
516
|
setInitialAudioRoute(callType)
|
|
470
517
|
updateLockScreenBypass()
|
|
471
518
|
|
|
472
|
-
// Emit call answered event with metadata
|
|
473
|
-
|
|
519
|
+
// Emit outgoing call answered event with metadata for JS-initiated calls
|
|
520
|
+
emitOutgoingCallAnsweredWithMetadata(callId)
|
|
474
521
|
}
|
|
475
522
|
|
|
476
523
|
// --- Call Answer Management ---
|
|
@@ -484,6 +531,7 @@ object CallEngine {
|
|
|
484
531
|
coreCallAnswered(callId, isLocalAnswer = true)
|
|
485
532
|
}
|
|
486
533
|
|
|
534
|
+
// FIXED: Core Call Answered Method with proper audio focus handling
|
|
487
535
|
private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
|
|
488
536
|
val context = requireContext()
|
|
489
537
|
Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
|
|
@@ -494,13 +542,27 @@ object CallEngine {
|
|
|
494
542
|
return
|
|
495
543
|
}
|
|
496
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
|
|
497
553
|
stopRingtone()
|
|
498
554
|
stopRingback()
|
|
499
555
|
cancelIncomingCallUI()
|
|
500
|
-
requestAudioFocus()
|
|
501
556
|
|
|
502
|
-
|
|
503
|
-
|
|
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
|
+
}
|
|
504
566
|
|
|
505
567
|
if (!canMakeMultipleCalls) {
|
|
506
568
|
activeCalls.filter { it.key != callId }.values.forEach { otherCall ->
|
|
@@ -511,18 +573,25 @@ object CallEngine {
|
|
|
511
573
|
}
|
|
512
574
|
|
|
513
575
|
bringAppToForeground()
|
|
514
|
-
startForegroundService()
|
|
576
|
+
startForegroundService()
|
|
515
577
|
keepScreenAwake(true)
|
|
516
578
|
resetAudioMode()
|
|
517
579
|
updateLockScreenBypass()
|
|
518
580
|
updateForegroundNotification()
|
|
519
581
|
|
|
520
|
-
// Emit
|
|
521
|
-
|
|
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
|
+
}
|
|
522
590
|
|
|
523
591
|
Log.d(TAG, "Call $callId successfully answered and UI cleaned up")
|
|
524
592
|
}
|
|
525
593
|
|
|
594
|
+
// For incoming calls (local answer)
|
|
526
595
|
private fun emitCallAnsweredWithMetadata(callId: String) {
|
|
527
596
|
val callInfo = activeCalls[callId] ?: return
|
|
528
597
|
val metadata = callMetadata[callId]
|
|
@@ -543,6 +612,27 @@ object CallEngine {
|
|
|
543
612
|
})
|
|
544
613
|
}
|
|
545
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
|
+
|
|
546
636
|
// --- Call Control Methods ---
|
|
547
637
|
fun holdCall(callId: String) {
|
|
548
638
|
holdCallInternal(callId, heldBySystem = false)
|
|
@@ -601,7 +691,7 @@ object CallEngine {
|
|
|
601
691
|
return
|
|
602
692
|
}
|
|
603
693
|
|
|
604
|
-
//
|
|
694
|
+
// FIXED: Simplified audio focus check to prevent UNHELD FAILED
|
|
605
695
|
if (!hasAudioFocus && !resumedBySystem && !requestAudioFocus()) {
|
|
606
696
|
Log.w(TAG, "Failed to get audio focus for unhold - but continuing anyway")
|
|
607
697
|
// Don't emit UNHELD FAILED - just continue
|