@qusaieilouti99/call-manager 0.1.67 → 0.1.69

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.
@@ -13,7 +13,6 @@ 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
17
16
  import android.media.AudioManager
18
17
  import android.media.MediaPlayer
19
18
  import android.media.RingtoneManager
@@ -31,7 +30,6 @@ import android.telecom.PhoneAccountHandle
31
30
  import android.telecom.TelecomManager
32
31
  import android.telecom.VideoProfile
33
32
  import android.util.Log
34
- import androidx.annotation.RequiresApi
35
33
  import kotlinx.coroutines.CoroutineScope
36
34
  import kotlinx.coroutines.Dispatchers
37
35
  import kotlinx.coroutines.launch
@@ -52,16 +50,11 @@ object CallEngine {
52
50
  private val isInitialized = AtomicBoolean(false)
53
51
  private val initializationLock = Any()
54
52
 
55
- // Enhanced Audio & Media Management
53
+ // Simplified Audio & Media Management (NO MANUAL AUDIO FOCUS)
56
54
  private var ringtone: android.media.Ringtone? = null
57
55
  private var ringbackPlayer: MediaPlayer? = null
58
56
  private var audioManager: AudioManager? = null
59
57
  private var wakeLock: PowerManager.WakeLock? = null
60
- private var audioFocusRequest: AudioFocusRequest? = null
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
65
58
 
66
59
  // Call State Management
67
60
  private val activeCalls = ConcurrentHashMap<String, CallInfo>()
@@ -86,38 +79,6 @@ object CallEngine {
86
79
  fun onLockScreenBypassChanged(shouldBypass: Boolean)
87
80
  }
88
81
 
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
-
121
82
  // --- INITIALIZATION ---
122
83
  fun initialize(context: Context) {
123
84
  synchronized(initializationLock) {
@@ -165,102 +126,6 @@ object CallEngine {
165
126
  }
166
127
  }
167
128
 
168
- // --- Enhanced Audio Focus Management for Self-Managed Calls ---
169
- private fun requestAudioFocus(): Boolean {
170
- val context = requireContext()
171
- audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
172
-
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()
182
- } else {
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)
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
230
- }
231
-
232
- private fun abandonAudioFocus() {
233
- if (!hasAudioFocus) return
234
-
235
- audioManager?.let { am ->
236
- val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
237
- audioFocusRequest?.let { request ->
238
- am.abandonAudioFocusRequest(request)
239
- } ?: AudioManager.AUDIOFOCUS_REQUEST_FAILED
240
- } else {
241
- @Suppress("DEPRECATION")
242
- am.abandonAudioFocus(audioFocusChangeListener)
243
- }
244
- Log.d(TAG, "Audio focus abandoned, result: $result")
245
- }
246
-
247
- hasAudioFocus = false
248
- audioFocusRetryCount = 0
249
- audioFocusRetryHandler.removeCallbacksAndMessages(null)
250
- }
251
-
252
- private fun holdAllActiveCalls(heldBySystem: Boolean) {
253
- activeCalls.values.filter { it.state == CallState.ACTIVE }.forEach { call ->
254
- holdCallInternal(call.callId, heldBySystem = heldBySystem)
255
- }
256
- }
257
-
258
- private fun resumeSystemHeldCalls() {
259
- activeCalls.values.filter { it.state == CallState.HELD && it.wasHeldBySystem }.forEach { call ->
260
- unholdCallInternal(call.callId, resumedBySystem = true)
261
- }
262
- }
263
-
264
129
  // --- Lock Screen Bypass Management ---
265
130
  fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
266
131
  lockScreenBypassCallbacks.add(callback)
@@ -387,7 +252,7 @@ object CallEngine {
387
252
  updateLockScreenBypass()
388
253
  }
389
254
 
390
- // --- Enhanced Outgoing Call Management ---
255
+ // --- Outgoing Call Management ---
391
256
  fun startOutgoingCall(
392
257
  callId: String,
393
258
  callType: String,
@@ -419,10 +284,8 @@ object CallEngine {
419
284
  currentCallId = callId
420
285
  Log.d(TAG, "Call $callId added to activeCalls. State: DIALING")
421
286
 
422
- // Set audio mode and request focus early for outgoing calls
287
+ // ONLY set audio mode - let system handle audio focus for self-managed calls
423
288
  setAudioMode()
424
- val audioFocusGranted = requestAudioFocus()
425
- Log.d(TAG, "Audio focus for outgoing call: $audioFocusGranted")
426
289
 
427
290
  registerPhoneAccount()
428
291
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
@@ -447,10 +310,8 @@ object CallEngine {
447
310
  telecomManager.placeCall(addressUri, extras)
448
311
  startForegroundService()
449
312
 
450
- // Start ringback only if audio focus is available
451
- if (audioFocusGranted) {
452
- startRingback()
453
- }
313
+ // Start ringback (system will handle audio focus)
314
+ startRingback()
454
315
 
455
316
  bringAppToForeground()
456
317
  keepScreenAwake(true)
@@ -494,7 +355,6 @@ object CallEngine {
494
355
 
495
356
  registerPhoneAccount()
496
357
  setAudioMode()
497
- requestAudioFocus()
498
358
  bringAppToForeground()
499
359
  startForegroundService()
500
360
  keepScreenAwake(true)
@@ -505,7 +365,7 @@ object CallEngine {
505
365
  emitOutgoingCallAnsweredWithMetadata(callId)
506
366
  }
507
367
 
508
- // --- Enhanced Call Answer Management ---
368
+ // --- Call Answer Management (SIMPLIFIED - NO MANUAL AUDIO FOCUS) ---
509
369
  fun callAnsweredFromJS(callId: String) {
510
370
  Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
511
371
  coreCallAnswered(callId, isLocalAnswer = false)
@@ -516,7 +376,7 @@ object CallEngine {
516
376
  coreCallAnswered(callId, isLocalAnswer = true)
517
377
  }
518
378
 
519
- // Enhanced call answer flow with proper audio focus timing
379
+ // SIMPLIFIED: Let system handle audio focus for self-managed calls
520
380
  private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
521
381
  Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
522
382
 
@@ -526,19 +386,13 @@ object CallEngine {
526
386
  return
527
387
  }
528
388
 
529
- // Set audio mode BEFORE requesting audio focus
389
+ // Set audio mode and let system handle audio focus
530
390
  setAudioMode()
531
391
 
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
392
+ // Set call to ACTIVE
539
393
  activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
540
394
  currentCallId = callId
541
- Log.d(TAG, "Call $callId set to ACTIVE state (audio focus: $audioFocusGranted)")
395
+ Log.d(TAG, "Call $callId set to ACTIVE state (system manages audio focus)")
542
396
 
543
397
  // Clean up media and UI
544
398
  stopRingtone()
@@ -660,11 +514,6 @@ object CallEngine {
660
514
  return
661
515
  }
662
516
 
663
- // Request audio focus when resuming a call
664
- if (resumedBySystem) {
665
- requestAudioFocus()
666
- }
667
-
668
517
  activeCalls[callId] = callInfo.copy(
669
518
  state = CallState.ACTIVE,
670
519
  wasHeldBySystem = false
@@ -780,7 +629,7 @@ object CallEngine {
780
629
  })
781
630
  }
782
631
 
783
- // --- Audio Management ---
632
+ // --- Enhanced Audio Management ---
784
633
  fun getAudioDevices(): AudioRoutesInfo {
785
634
  val context = requireContext()
786
635
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
@@ -788,8 +637,12 @@ object CallEngine {
788
637
  }
789
638
 
790
639
  val devices = mutableSetOf<String>()
791
- var currentRoute = "Earpiece"
792
640
 
641
+ // ALWAYS include Speaker and Earpiece for phone calls
642
+ devices.add("Speaker")
643
+ devices.add("Earpiece")
644
+
645
+ // Check for additional connected devices
793
646
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
794
647
  val audioDeviceInfoList = audioManager?.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
795
648
  audioDeviceInfoList?.forEach { device ->
@@ -800,24 +653,21 @@ object CallEngine {
800
653
  AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET -> {
801
654
  devices.add("Headset")
802
655
  }
803
- AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> {
804
- devices.add("Speaker")
805
- }
806
- AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> {
807
- devices.add("Earpiece")
808
- }
656
+ // Speaker and Earpiece already added above
809
657
  }
810
658
  }
811
659
  } else {
812
- devices.addAll(listOf("Speaker", "Earpiece"))
660
+ // For older versions, check for Bluetooth and Headset
661
+ if (audioManager?.isBluetoothA2dpOn == true || audioManager?.isBluetoothScoOn == true) {
662
+ devices.add("Bluetooth")
663
+ }
664
+ if (audioManager?.isWiredHeadsetOn == true) {
665
+ devices.add("Headset")
666
+ }
813
667
  }
814
668
 
815
- currentRoute = when {
816
- audioManager?.isBluetoothScoOn == true -> "Bluetooth"
817
- audioManager?.isSpeakerphoneOn == true -> "Speaker"
818
- audioManager?.isWiredHeadsetOn == true -> "Headset"
819
- else -> "Earpiece"
820
- }
669
+ val currentRoute = getCurrentAudioRoute()
670
+ Log.d(TAG, "Available audio devices: ${devices.toList()}, current route: $currentRoute")
821
671
 
822
672
  return AudioRoutesInfo(devices.toTypedArray(), currentRoute)
823
673
  }
@@ -829,6 +679,7 @@ object CallEngine {
829
679
 
830
680
  val previousRoute = getCurrentAudioRoute()
831
681
 
682
+ // Reset all routes first
832
683
  audioManager?.isSpeakerphoneOn = false
833
684
  audioManager?.stopBluetoothSco()
834
685
  audioManager?.isBluetoothScoOn = false
@@ -840,6 +691,7 @@ object CallEngine {
840
691
  }
841
692
  "Earpiece" -> {
842
693
  audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
694
+ // Earpiece is the default - just ensure speaker and bluetooth are off
843
695
  }
844
696
  "Bluetooth" -> {
845
697
  audioManager?.startBluetoothSco()
@@ -848,6 +700,7 @@ object CallEngine {
848
700
  }
849
701
  "Headset" -> {
850
702
  audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
703
+ // Headset routing is automatic when connected
851
704
  }
852
705
  else -> {
853
706
  Log.w(TAG, "Unknown audio route: $route")
@@ -855,12 +708,30 @@ object CallEngine {
855
708
  }
856
709
  }
857
710
 
711
+ val newRoute = getCurrentAudioRoute()
712
+ if (previousRoute != newRoute) {
713
+ // Emit unified event with full context
714
+ emitAudioRouteChanged()
715
+ }
716
+ }
717
+
858
718
  val newRoute = getCurrentAudioRoute()
859
719
  if (previousRoute != newRoute) {
860
720
  emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, JSONObject().put("route", newRoute))
861
721
  }
862
722
  }
863
723
 
724
+ // UNIFIED event emission - always sends full audio context
725
+ private fun emitAudioRouteChanged() {
726
+ val audioInfo = getAudioDevices()
727
+ val jsonPayload = JSONObject().apply {
728
+ put("devices", JSONArray(audioInfo.devices.toList()))
729
+ put("currentRoute", audioInfo.currentRoute)
730
+ }
731
+ emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, jsonPayload)
732
+ Log.d(TAG, "Audio route changed: ${audioInfo.currentRoute}, available: ${audioInfo.devices.toList()}")
733
+ }
734
+
864
735
  private fun getCurrentAudioRoute(): String {
865
736
  return when {
866
737
  audioManager?.isBluetoothScoOn == true -> "Bluetooth"
@@ -886,6 +757,7 @@ object CallEngine {
886
757
 
887
758
  private fun setAudioMode() {
888
759
  audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
760
+ Log.d(TAG, "Audio mode set to MODE_IN_COMMUNICATION (system handles audio focus)")
889
761
  }
890
762
 
891
763
  private fun resetAudioMode() {
@@ -894,21 +766,34 @@ object CallEngine {
894
766
  audioManager?.stopBluetoothSco()
895
767
  audioManager?.isBluetoothScoOn = false
896
768
  audioManager?.isSpeakerphoneOn = false
897
- abandonAudioFocus()
769
+ Log.d(TAG, "Audio mode reset to MODE_NORMAL")
898
770
  }
899
771
  }
900
772
 
901
- // --- Audio Device Callback ---
773
+ // --- Audio Device Callback (simplified) ---
902
774
  private val audioDeviceCallback = object : AudioDeviceCallback() {
903
775
  override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
904
- emitAudioDevicesChangedIfNeeded()
776
+ Log.d(TAG, "Audio devices added")
777
+ emitAudioDevicesChanged()
905
778
  }
906
779
 
907
780
  override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
908
- emitAudioDevicesChangedIfNeeded()
781
+ Log.d(TAG, "Audio devices removed")
782
+ emitAudioDevicesChanged()
909
783
  }
910
784
  }
911
785
 
786
+ // Separate event for when physical devices are added/removed
787
+ private fun emitAudioDevicesChanged() {
788
+ val audioInfo = getAudioDevices()
789
+ val jsonPayload = JSONObject().apply {
790
+ put("devices", JSONArray(audioInfo.devices.toList()))
791
+ put("currentRoute", audioInfo.currentRoute)
792
+ }
793
+ emitEvent(CallEventType.AUDIO_DEVICES_CHANGED, jsonPayload)
794
+ Log.d(TAG, "Audio devices changed: available: ${audioInfo.devices.toList()}")
795
+ }
796
+
912
797
  fun registerAudioDeviceCallback() {
913
798
  val context = requireContext()
914
799
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
@@ -921,22 +806,6 @@ object CallEngine {
921
806
  audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
922
807
  }
923
808
 
924
- private fun emitAudioDevicesChangedIfNeeded() {
925
- val currentAudioInfo = getAudioDevices()
926
-
927
- if (lastAudioRoutesInfo == null ||
928
- !currentAudioInfo.devices.contentEquals(lastAudioRoutesInfo!!.devices) ||
929
- currentAudioInfo.currentRoute != lastAudioRoutesInfo!!.currentRoute) {
930
-
931
- lastAudioRoutesInfo = currentAudioInfo
932
- val jsonPayload = JSONObject().apply {
933
- put("devices", JSONArray(currentAudioInfo.devices.toList()))
934
- put("currentRoute", currentAudioInfo.currentRoute)
935
- }
936
- emitEvent(CallEventType.AUDIO_DEVICES_CHANGED, jsonPayload)
937
- }
938
- }
939
-
940
809
  // --- Screen Management ---
941
810
  fun keepScreenAwake(keepAwake: Boolean) {
942
811
  val context = requireContext()
@@ -84,19 +84,11 @@ class MyConnection(
84
84
  }
85
85
  }
86
86
 
87
+ // Only emit route change if route actually changed
87
88
  if (lastAudioState == null || lastAudioState!!.route != state.route) {
88
- val routeName = when (state.route) {
89
- CallAudioState.ROUTE_SPEAKER -> "Speaker"
90
- CallAudioState.ROUTE_EARPIECE -> "Earpiece"
91
- CallAudioState.ROUTE_BLUETOOTH -> "Bluetooth"
92
- CallAudioState.ROUTE_WIRED_HEADSET -> "Headset"
93
- else -> "Unknown"
94
- }
95
-
96
- CallEngine.emitEvent(
97
- CallEventType.AUDIO_ROUTE_CHANGED,
98
- JSONObject().put("callId", callId).put("route", routeName)
99
- )
89
+ // Don't emit here - let CallEngine handle it to avoid duplication
90
+ // The system audio state change will be detected by CallEngine's audio management
91
+ Log.d(TAG, "System audio route changed to: ${state.route} for callId: $callId")
100
92
  }
101
93
 
102
94
  lastAudioState = state
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",