@qusaieilouti99/call-manager 0.1.66 → 0.1.67
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.
|
@@ -31,6 +31,7 @@ import android.telecom.PhoneAccountHandle
|
|
|
31
31
|
import android.telecom.TelecomManager
|
|
32
32
|
import android.telecom.VideoProfile
|
|
33
33
|
import android.util.Log
|
|
34
|
+
import androidx.annotation.RequiresApi
|
|
34
35
|
import kotlinx.coroutines.CoroutineScope
|
|
35
36
|
import kotlinx.coroutines.Dispatchers
|
|
36
37
|
import kotlinx.coroutines.launch
|
|
@@ -51,13 +52,16 @@ object CallEngine {
|
|
|
51
52
|
private val isInitialized = AtomicBoolean(false)
|
|
52
53
|
private val initializationLock = Any()
|
|
53
54
|
|
|
54
|
-
// Audio & Media
|
|
55
|
+
// Enhanced Audio & Media Management
|
|
55
56
|
private var ringtone: android.media.Ringtone? = null
|
|
56
57
|
private var ringbackPlayer: MediaPlayer? = null
|
|
57
58
|
private var audioManager: AudioManager? = null
|
|
58
59
|
private var wakeLock: PowerManager.WakeLock? = null
|
|
59
60
|
private var audioFocusRequest: AudioFocusRequest? = null
|
|
60
61
|
private var hasAudioFocus: Boolean = false
|
|
62
|
+
private val audioFocusRetryHandler = Handler(Looper.getMainLooper())
|
|
63
|
+
private var audioFocusRetryCount = 0
|
|
64
|
+
private val MAX_AUDIO_FOCUS_RETRIES = 3
|
|
61
65
|
|
|
62
66
|
// Call State Management
|
|
63
67
|
private val activeCalls = ConcurrentHashMap<String, CallInfo>()
|
|
@@ -67,7 +71,7 @@ object CallEngine {
|
|
|
67
71
|
private var currentCallId: String? = null
|
|
68
72
|
private var canMakeMultipleCalls: Boolean = false
|
|
69
73
|
|
|
70
|
-
// Audio State Tracking
|
|
74
|
+
// Audio State Tracking
|
|
71
75
|
private var lastAudioRoutesInfo: AudioRoutesInfo? = null
|
|
72
76
|
|
|
73
77
|
// Lock Screen Bypass
|
|
@@ -82,6 +86,38 @@ object CallEngine {
|
|
|
82
86
|
fun onLockScreenBypassChanged(shouldBypass: Boolean)
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
// Enhanced Audio Focus Change Listener for Self-Managed Calls
|
|
90
|
+
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
|
91
|
+
Log.d(TAG, "Audio focus changed: $focusChange")
|
|
92
|
+
when (focusChange) {
|
|
93
|
+
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
94
|
+
Log.d(TAG, "Audio focus gained")
|
|
95
|
+
hasAudioFocus = true
|
|
96
|
+
audioFocusRetryCount = 0
|
|
97
|
+
|
|
98
|
+
// Resume any system-held calls after a short delay
|
|
99
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
100
|
+
resumeSystemHeldCalls()
|
|
101
|
+
}, 500)
|
|
102
|
+
}
|
|
103
|
+
AudioManager.AUDIOFOCUS_LOSS -> {
|
|
104
|
+
Log.d(TAG, "Permanent audio focus loss - holding active calls")
|
|
105
|
+
hasAudioFocus = false
|
|
106
|
+
holdAllActiveCalls(heldBySystem = true)
|
|
107
|
+
}
|
|
108
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
109
|
+
Log.d(TAG, "Transient audio focus loss - holding calls temporarily")
|
|
110
|
+
hasAudioFocus = false
|
|
111
|
+
holdAllActiveCalls(heldBySystem = true)
|
|
112
|
+
}
|
|
113
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
|
114
|
+
Log.d(TAG, "Transient audio focus loss (can duck) - keeping calls active")
|
|
115
|
+
hasAudioFocus = false
|
|
116
|
+
// Don't hold calls for ducking scenarios in self-managed calls
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
85
121
|
// --- INITIALIZATION ---
|
|
86
122
|
fun initialize(context: Context) {
|
|
87
123
|
synchronized(initializationLock) {
|
|
@@ -129,78 +165,88 @@ object CallEngine {
|
|
|
129
165
|
}
|
|
130
166
|
}
|
|
131
167
|
|
|
132
|
-
// ---
|
|
133
|
-
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
|
134
|
-
Log.d(TAG, "Audio focus changed: $focusChange")
|
|
135
|
-
when (focusChange) {
|
|
136
|
-
AudioManager.AUDIOFOCUS_LOSS -> {
|
|
137
|
-
// Only hold calls on PERMANENT audio focus loss (like system calls)
|
|
138
|
-
Log.d(TAG, "Permanent audio focus loss - holding calls")
|
|
139
|
-
hasAudioFocus = false
|
|
140
|
-
holdAllActiveCalls(heldBySystem = true)
|
|
141
|
-
}
|
|
142
|
-
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
143
|
-
Log.d(TAG, "Audio focus gained - resuming held calls")
|
|
144
|
-
hasAudioFocus = true
|
|
145
|
-
Handler(Looper.getMainLooper()).postDelayed({
|
|
146
|
-
resumeSystemHeldCalls()
|
|
147
|
-
}, 1000)
|
|
148
|
-
}
|
|
149
|
-
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT,
|
|
150
|
-
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
|
151
|
-
// Don't hold calls for transient losses - just note we lost focus
|
|
152
|
-
Log.d(TAG, "Transient audio focus loss - keeping calls active")
|
|
153
|
-
hasAudioFocus = false
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
168
|
+
// --- Enhanced Audio Focus Management for Self-Managed Calls ---
|
|
158
169
|
private fun requestAudioFocus(): Boolean {
|
|
159
170
|
val context = requireContext()
|
|
160
171
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
161
172
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
.setOnAudioFocusChangeListener(audioFocusChangeListener)
|
|
172
|
-
.setAcceptsDelayedFocusGain(true)
|
|
173
|
-
.build()
|
|
174
|
-
}
|
|
175
|
-
val result = audioManager?.requestAudioFocus(audioFocusRequest!!)
|
|
176
|
-
hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
177
|
-
Log.d(TAG, "Audio focus request result: $result (granted: $hasAudioFocus)")
|
|
178
|
-
hasAudioFocus
|
|
173
|
+
if (hasAudioFocus) {
|
|
174
|
+
Log.d(TAG, "Audio focus already granted")
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Log.d(TAG, "Requesting audio focus for self-managed call (attempt ${audioFocusRetryCount + 1})")
|
|
179
|
+
|
|
180
|
+
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
181
|
+
requestAudioFocusApi26Plus()
|
|
179
182
|
} else {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
183
|
+
requestAudioFocusLegacy()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
val success = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
187
|
+
hasAudioFocus = success
|
|
188
|
+
|
|
189
|
+
Log.d(TAG, "Audio focus request result: $result (granted: $success)")
|
|
190
|
+
|
|
191
|
+
if (!success && audioFocusRetryCount < MAX_AUDIO_FOCUS_RETRIES) {
|
|
192
|
+
// Retry after a short delay
|
|
193
|
+
audioFocusRetryCount++
|
|
194
|
+
audioFocusRetryHandler.postDelayed({
|
|
195
|
+
Log.d(TAG, "Retrying audio focus request...")
|
|
196
|
+
requestAudioFocus()
|
|
197
|
+
}, 200)
|
|
188
198
|
}
|
|
199
|
+
|
|
200
|
+
return success
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@RequiresApi(Build.VERSION_CODES.O)
|
|
204
|
+
private fun requestAudioFocusApi26Plus(): Int {
|
|
205
|
+
if (audioFocusRequest == null) {
|
|
206
|
+
audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
|
|
207
|
+
.setAudioAttributes(
|
|
208
|
+
AudioAttributes.Builder()
|
|
209
|
+
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
|
|
210
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
211
|
+
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
|
|
212
|
+
.build()
|
|
213
|
+
)
|
|
214
|
+
.setOnAudioFocusChangeListener(audioFocusChangeListener)
|
|
215
|
+
.setAcceptsDelayedFocusGain(true)
|
|
216
|
+
.setWillPauseWhenDucked(false)
|
|
217
|
+
.build()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return audioManager?.requestAudioFocus(audioFocusRequest!!) ?: AudioManager.AUDIOFOCUS_REQUEST_FAILED
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@Suppress("DEPRECATION")
|
|
224
|
+
private fun requestAudioFocusLegacy(): Int {
|
|
225
|
+
return audioManager?.requestAudioFocus(
|
|
226
|
+
audioFocusChangeListener,
|
|
227
|
+
AudioManager.STREAM_VOICE_CALL,
|
|
228
|
+
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
|
|
229
|
+
) ?: AudioManager.AUDIOFOCUS_REQUEST_FAILED
|
|
189
230
|
}
|
|
190
231
|
|
|
191
232
|
private fun abandonAudioFocus() {
|
|
233
|
+
if (!hasAudioFocus) return
|
|
234
|
+
|
|
192
235
|
audioManager?.let { am ->
|
|
193
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
236
|
+
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
194
237
|
audioFocusRequest?.let { request ->
|
|
195
238
|
am.abandonAudioFocusRequest(request)
|
|
196
|
-
}
|
|
239
|
+
} ?: AudioManager.AUDIOFOCUS_REQUEST_FAILED
|
|
197
240
|
} else {
|
|
198
241
|
@Suppress("DEPRECATION")
|
|
199
242
|
am.abandonAudioFocus(audioFocusChangeListener)
|
|
200
243
|
}
|
|
244
|
+
Log.d(TAG, "Audio focus abandoned, result: $result")
|
|
201
245
|
}
|
|
246
|
+
|
|
202
247
|
hasAudioFocus = false
|
|
203
|
-
|
|
248
|
+
audioFocusRetryCount = 0
|
|
249
|
+
audioFocusRetryHandler.removeCallbacksAndMessages(null)
|
|
204
250
|
}
|
|
205
251
|
|
|
206
252
|
private fun holdAllActiveCalls(heldBySystem: Boolean) {
|
|
@@ -341,7 +387,7 @@ object CallEngine {
|
|
|
341
387
|
updateLockScreenBypass()
|
|
342
388
|
}
|
|
343
389
|
|
|
344
|
-
// --- Outgoing Call Management ---
|
|
390
|
+
// --- Enhanced Outgoing Call Management ---
|
|
345
391
|
fun startOutgoingCall(
|
|
346
392
|
callId: String,
|
|
347
393
|
callType: String,
|
|
@@ -373,6 +419,11 @@ object CallEngine {
|
|
|
373
419
|
currentCallId = callId
|
|
374
420
|
Log.d(TAG, "Call $callId added to activeCalls. State: DIALING")
|
|
375
421
|
|
|
422
|
+
// Set audio mode and request focus early for outgoing calls
|
|
423
|
+
setAudioMode()
|
|
424
|
+
val audioFocusGranted = requestAudioFocus()
|
|
425
|
+
Log.d(TAG, "Audio focus for outgoing call: $audioFocusGranted")
|
|
426
|
+
|
|
376
427
|
registerPhoneAccount()
|
|
377
428
|
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
378
429
|
val phoneAccountHandle = getPhoneAccountHandle()
|
|
@@ -396,9 +447,10 @@ object CallEngine {
|
|
|
396
447
|
telecomManager.placeCall(addressUri, extras)
|
|
397
448
|
startForegroundService()
|
|
398
449
|
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
450
|
+
// Start ringback only if audio focus is available
|
|
451
|
+
if (audioFocusGranted) {
|
|
452
|
+
startRingback()
|
|
453
|
+
}
|
|
402
454
|
|
|
403
455
|
bringAppToForeground()
|
|
404
456
|
keepScreenAwake(true)
|
|
@@ -441,6 +493,7 @@ object CallEngine {
|
|
|
441
493
|
Log.d(TAG, "Call $callId started as ACTIVE")
|
|
442
494
|
|
|
443
495
|
registerPhoneAccount()
|
|
496
|
+
setAudioMode()
|
|
444
497
|
requestAudioFocus()
|
|
445
498
|
bringAppToForeground()
|
|
446
499
|
startForegroundService()
|
|
@@ -452,7 +505,7 @@ object CallEngine {
|
|
|
452
505
|
emitOutgoingCallAnsweredWithMetadata(callId)
|
|
453
506
|
}
|
|
454
507
|
|
|
455
|
-
// --- Call Answer Management
|
|
508
|
+
// --- Enhanced Call Answer Management ---
|
|
456
509
|
fun callAnsweredFromJS(callId: String) {
|
|
457
510
|
Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
|
|
458
511
|
coreCallAnswered(callId, isLocalAnswer = false)
|
|
@@ -463,7 +516,7 @@ object CallEngine {
|
|
|
463
516
|
coreCallAnswered(callId, isLocalAnswer = true)
|
|
464
517
|
}
|
|
465
518
|
|
|
466
|
-
//
|
|
519
|
+
// Enhanced call answer flow with proper audio focus timing
|
|
467
520
|
private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
|
|
468
521
|
Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
|
|
469
522
|
|
|
@@ -473,19 +526,26 @@ object CallEngine {
|
|
|
473
526
|
return
|
|
474
527
|
}
|
|
475
528
|
|
|
476
|
-
//
|
|
529
|
+
// Set audio mode BEFORE requesting audio focus
|
|
530
|
+
setAudioMode()
|
|
531
|
+
|
|
532
|
+
// Request audio focus BEFORE setting call to active
|
|
533
|
+
val audioFocusGranted = requestAudioFocus()
|
|
534
|
+
if (!audioFocusGranted) {
|
|
535
|
+
Log.w(TAG, "Audio focus not granted for call $callId, but proceeding anyway")
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Now set call to ACTIVE
|
|
477
539
|
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
478
540
|
currentCallId = callId
|
|
479
|
-
Log.d(TAG, "Call $callId set to ACTIVE state")
|
|
541
|
+
Log.d(TAG, "Call $callId set to ACTIVE state (audio focus: $audioFocusGranted)")
|
|
480
542
|
|
|
481
543
|
// Clean up media and UI
|
|
482
544
|
stopRingtone()
|
|
483
545
|
stopRingback()
|
|
484
546
|
cancelIncomingCallUI()
|
|
485
547
|
|
|
486
|
-
//
|
|
487
|
-
requestAudioFocus()
|
|
488
|
-
|
|
548
|
+
// Handle multiple calls
|
|
489
549
|
if (!canMakeMultipleCalls) {
|
|
490
550
|
activeCalls.filter { it.key != callId }.values.forEach { otherCall ->
|
|
491
551
|
if (otherCall.state == CallState.ACTIVE) {
|
|
@@ -497,10 +557,9 @@ object CallEngine {
|
|
|
497
557
|
bringAppToForeground()
|
|
498
558
|
startForegroundService()
|
|
499
559
|
keepScreenAwake(true)
|
|
500
|
-
setAudioMode()
|
|
501
560
|
updateLockScreenBypass()
|
|
502
561
|
|
|
503
|
-
//
|
|
562
|
+
// Emit events based on call direction
|
|
504
563
|
if (isLocalAnswer) {
|
|
505
564
|
emitCallAnsweredWithMetadata(callId)
|
|
506
565
|
} else {
|
|
@@ -601,6 +660,11 @@ object CallEngine {
|
|
|
601
660
|
return
|
|
602
661
|
}
|
|
603
662
|
|
|
663
|
+
// Request audio focus when resuming a call
|
|
664
|
+
if (resumedBySystem) {
|
|
665
|
+
requestAudioFocus()
|
|
666
|
+
}
|
|
667
|
+
|
|
604
668
|
activeCalls[callId] = callInfo.copy(
|
|
605
669
|
state = CallState.ACTIVE,
|
|
606
670
|
wasHeldBySystem = false
|
|
@@ -1028,7 +1092,7 @@ object CallEngine {
|
|
|
1028
1092
|
stopRingtone()
|
|
1029
1093
|
}
|
|
1030
1094
|
|
|
1031
|
-
// --- Service Management
|
|
1095
|
+
// --- Service Management ---
|
|
1032
1096
|
private fun startForegroundService() {
|
|
1033
1097
|
val context = requireContext()
|
|
1034
1098
|
val currentCall = activeCalls.values.find {
|
|
@@ -1161,7 +1225,7 @@ object CallEngine {
|
|
|
1161
1225
|
}
|
|
1162
1226
|
}
|
|
1163
1227
|
|
|
1164
|
-
// --- Cleanup
|
|
1228
|
+
// --- Cleanup ---
|
|
1165
1229
|
private fun cleanup() {
|
|
1166
1230
|
Log.d(TAG, "Performing cleanup")
|
|
1167
1231
|
stopForegroundService()
|