@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 - SIMPLIFIED
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 - SIMPLIFIED
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
- // --- SIMPLIFIED Audio Focus Management ---
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
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
163
- if (audioFocusRequest == null) {
164
- audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
165
- .setAudioAttributes(
166
- AudioAttributes.Builder()
167
- .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
168
- .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
169
- .build()
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
- @Suppress("DEPRECATION")
181
- val result = audioManager?.requestAudioFocus(
182
- audioFocusChangeListener,
183
- AudioManager.STREAM_VOICE_CALL,
184
- AudioManager.AUDIOFOCUS_GAIN
185
- )
186
- hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
187
- hasAudioFocus
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
- Log.d(TAG, "Audio focus abandoned")
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
- // SIMPLIFIED: Request audio focus and start ringback
400
- requestAudioFocus()
401
- startRingback()
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 - SIMPLIFIED ---
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
- // SIMPLIFIED: Always succeed call answer, handle audio focus gracefully
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
- // ALWAYS set call to ACTIVE - don't fail due to audio focus
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
- // Request audio focus (but don't fail if not granted)
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
- // Always emit events based on call direction
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 - SIMPLIFIED ---
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 - SIMPLIFIED ---
1228
+ // --- Cleanup ---
1165
1229
  private fun cleanup() {
1166
1230
  Log.d(TAG, "Performing cleanup")
1167
1231
  stopForegroundService()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.66",
3
+ "version": "0.1.67",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",