@qusaieilouti99/call-manager 0.1.150 → 0.1.152

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.
@@ -1,5 +1,5 @@
1
1
  package com.margelo.nitro.qusaieilouti99.callmanager
2
-
2
+ import android.telecom.CallAudioState
3
3
  import android.app.ActivityManager
4
4
  import android.app.Notification
5
5
  import android.app.NotificationChannel
@@ -40,8 +40,11 @@ import android.os.VibrationEffect
40
40
  * Core call‐management engine. Manages self-managed telecom calls,
41
41
  * audio routing, UI notifications, etc.
42
42
  *
43
- * NOTE: Volume key silencing is now handled by the system via `Connection.onSilence()`,
44
- * which calls `silenceIncomingCall()` on this object.
43
+ * Audio routing follows Android standards:
44
+ * - Audio calls default to earpiece unless BT/headset connected
45
+ * - Video calls default to speaker unless BT/headset connected
46
+ * - System handles route changes when devices connect/disconnect
47
+ * - Manual route changes are always respected
45
48
  */
46
49
  object CallEngine {
47
50
  private const val TAG = "CallEngine"
@@ -80,12 +83,16 @@ object CallEngine {
80
83
 
81
84
  private var currentCallId: String? = null
82
85
  private var canMakeMultipleCalls: Boolean = false
83
- private var lastAudioRoutesInfo: AudioRoutesInfo? = null
84
86
  private var lockScreenBypassActive = false
85
87
  private val lockScreenBypassCallbacks = mutableSetOf<LockScreenBypassCallback>()
86
88
  private var eventHandler: ((CallEventType, String) -> Unit)? = null
87
89
  private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
88
90
 
91
+ // Audio routing state
92
+ private var currentAudioRoute: String = "Earpiece"
93
+ private var wasManuallySet: Boolean = false
94
+ private var callStartTime: Long = 0
95
+
89
96
  interface LockScreenBypassCallback {
90
97
  fun onLockScreenBypassChanged(shouldBypass: Boolean)
91
98
  }
@@ -111,9 +118,6 @@ object CallEngine {
111
118
  )
112
119
  }
113
120
 
114
- /**
115
- * Get the application context. Returns null if not initialized.
116
- */
117
121
  fun getContext(): Context? = appContext
118
122
 
119
123
  fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
@@ -139,16 +143,12 @@ object CallEngine {
139
143
  }
140
144
  }
141
145
 
142
- /**
143
- * NEW: Check if device supports CallStyle notifications
144
- */
145
146
  private fun supportsCallStyleNotifications(): Boolean {
146
147
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
147
148
 
148
149
  val manufacturer = Build.MANUFACTURER.lowercase()
149
150
  val brand = Build.BRAND.lowercase()
150
151
 
151
- // Known good manufacturers that support CallStyle properly
152
152
  val supportedManufacturers = setOf(
153
153
  "google", "samsung", "oneplus", "motorola", "sony", "lg", "htc"
154
154
  )
@@ -166,10 +166,6 @@ object CallEngine {
166
166
  return isSupported
167
167
  }
168
168
 
169
- /**
170
- * Silences the incoming call ringtone. This is called by `Connection.onSilence()`
171
- * when the user presses a volume key during ringing.
172
- */
173
169
  fun silenceIncomingCall() {
174
170
  Log.d(TAG, "Silencing incoming call ringtone via Connection.onSilence()")
175
171
  stopRingtone()
@@ -391,6 +387,8 @@ object CallEngine {
391
387
  activeCalls[callId] =
392
388
  CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
393
389
  currentCallId = callId
390
+ callStartTime = System.currentTimeMillis()
391
+ wasManuallySet = false
394
392
  Log.d(TAG, "Call $callId started as ACTIVE")
395
393
 
396
394
  registerPhoneAccount()
@@ -399,7 +397,10 @@ object CallEngine {
399
397
  startForegroundService()
400
398
  keepScreenAwake(true)
401
399
 
402
- // NEW: Improved initial audio route setting with better timing
400
+ // Register audio device callback to handle dynamic device changes
401
+ registerAudioDeviceCallback()
402
+
403
+ // Set initial audio route based on call type and available devices
403
404
  mainHandler.postDelayed({
404
405
  setInitialAudioRoute(callType)
405
406
  }, 500L)
@@ -428,6 +429,8 @@ object CallEngine {
428
429
 
429
430
  activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
430
431
  currentCallId = callId
432
+ callStartTime = System.currentTimeMillis()
433
+ wasManuallySet = false
431
434
  Log.d(TAG, "Call $callId set to ACTIVE state")
432
435
 
433
436
  stopRingtone()
@@ -448,6 +451,11 @@ object CallEngine {
448
451
  updateLockScreenBypass()
449
452
 
450
453
  setAudioMode()
454
+
455
+ // Register audio device callback to handle dynamic device changes
456
+ registerAudioDeviceCallback()
457
+
458
+ // Set initial audio route with proper timing
451
459
  mainHandler.postDelayed({
452
460
  setInitialAudioRoute(callInfo.callType)
453
461
  }, 800L)
@@ -679,119 +687,262 @@ object CallEngine {
679
687
  })
680
688
  }
681
689
 
690
+ // ====== IMPROVED AUDIO ROUTING SYSTEM ======
691
+
682
692
  fun getAudioDevices(): AudioRoutesInfo {
683
693
  val context = requireContext()
684
694
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
685
695
  ?: return AudioRoutesInfo(emptyArray(), "Unknown")
686
696
 
687
697
  val devices = mutableSetOf<String>()
688
- devices.add("Speaker")
698
+ var hasWiredHeadset = false
699
+ var hasBluetoothDevice = false
700
+
701
+ // Always available
689
702
  devices.add("Earpiece")
703
+ devices.add("Speaker")
690
704
 
691
705
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
692
- val infos = audioManager?.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
693
- infos?.forEach { d ->
694
- when (d.type) {
706
+ val outputDevices = audioManager?.getDevices(AudioManager.GET_DEVICES_OUTPUTS) ?: emptyArray()
707
+ for (device in outputDevices) {
708
+ when (device.type) {
695
709
  AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
696
- AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add("Bluetooth")
710
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> {
711
+ if (!hasBluetoothDevice) {
712
+ devices.add("Bluetooth")
713
+ hasBluetoothDevice = true
714
+ }
715
+ }
697
716
  AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
698
- AudioDeviceInfo.TYPE_WIRED_HEADSET -> devices.add("Headset")
717
+ AudioDeviceInfo.TYPE_WIRED_HEADSET,
718
+ AudioDeviceInfo.TYPE_USB_HEADSET -> {
719
+ if (!hasWiredHeadset) {
720
+ devices.add("Headset")
721
+ hasWiredHeadset = true
722
+ }
723
+ }
724
+ AudioDeviceInfo.TYPE_BLE_HEADSET -> {
725
+ if (!hasBluetoothDevice) {
726
+ devices.add("Bluetooth")
727
+ hasBluetoothDevice = true
728
+ }
729
+ }
699
730
  }
700
731
  }
701
732
  } else {
733
+ // Fallback for older API levels
702
734
  @Suppress("DEPRECATION")
703
- if (audioManager?.isBluetoothA2dpOn == true || audioManager?.isBluetoothScoOn == true)
735
+ if (audioManager?.isBluetoothA2dpOn == true || audioManager?.isBluetoothScoOn == true) {
704
736
  devices.add("Bluetooth")
737
+ hasBluetoothDevice = true
738
+ }
705
739
  @Suppress("DEPRECATION")
706
- if (audioManager?.isWiredHeadsetOn == true) devices.add("Headset")
740
+ if (audioManager?.isWiredHeadsetOn == true) {
741
+ devices.add("Headset")
742
+ hasWiredHeadset = true
743
+ }
707
744
  }
708
745
 
709
746
  val current = getCurrentAudioRoute()
710
747
  Log.d(TAG, "Available audio devices: ${devices.toList()}, current: $current")
711
748
 
712
- // Convert strings to StringHolder objects
713
749
  val deviceHolders = devices.map { StringHolder(it) }.toTypedArray()
714
- lastAudioRoutesInfo = AudioRoutesInfo(deviceHolders, current)
715
750
  return AudioRoutesInfo(deviceHolders, current)
716
751
  }
717
752
 
718
- fun setAudioRoute(route: String) {
719
- Log.d(TAG, "setAudioRoute called: $route")
753
+ // NEW: Handle telecom audio route changes
754
+ fun onTelecomAudioRouteChanged(callId: String, audioState: CallAudioState) {
755
+ Log.d(TAG, "Telecom audio route changed for $callId: route=${audioState.route}")
720
756
 
757
+ val routeString = when (audioState.route) {
758
+ CallAudioState.ROUTE_EARPIECE -> "Earpiece"
759
+ CallAudioState.ROUTE_SPEAKER -> "Speaker"
760
+ CallAudioState.ROUTE_BLUETOOTH -> "Bluetooth"
761
+ CallAudioState.ROUTE_WIRED_HEADSET -> "Headset"
762
+ else -> "Unknown"
763
+ }
764
+
765
+ Log.d(TAG, "Emitting AUDIO_ROUTE_CHANGED: currentRoute=$routeString")
766
+ emitAudioRouteChanged(routeString)
767
+ }
768
+
769
+ // NEW: Set initial audio route using telecom
770
+ fun setInitialAudioRouteForCall(callId: String, callType: String) {
771
+ Log.d(TAG, "Setting initial audio route for $callId, type: $callType")
772
+
773
+ // Determine the desired route
774
+ val desiredRoute = when {
775
+ isBluetoothDeviceConnected() -> CallAudioState.ROUTE_BLUETOOTH
776
+ isWiredHeadsetConnected() -> CallAudioState.ROUTE_WIRED_HEADSET
777
+ callType.equals("Video", ignoreCase = true) -> CallAudioState.ROUTE_SPEAKER
778
+ else -> CallAudioState.ROUTE_EARPIECE
779
+ }
780
+
781
+ // Use telecom connection to set the route
782
+ telecomConnections[callId]?.let { connection ->
783
+ if (connection is MyConnection) {
784
+ mainHandler.postDelayed({
785
+ connection.setTelecomAudioRoute(desiredRoute)
786
+ Log.d(TAG, "Set initial telecom audio route to: $desiredRoute")
787
+ }, 200)
788
+ }
789
+ }
790
+ }
791
+
792
+ // UPDATED: Use telecom for manual route changes
793
+ fun setAudioRoute(route: String) {
794
+ Log.d(TAG, "setAudioRoute called: $route (manual)")
795
+ wasManuallySet = true
796
+
797
+ val telecomRoute = when (route) {
798
+ "Speaker" -> CallAudioState.ROUTE_SPEAKER
799
+ "Earpiece" -> CallAudioState.ROUTE_EARPIECE
800
+ "Bluetooth" -> CallAudioState.ROUTE_BLUETOOTH
801
+ "Headset" -> CallAudioState.ROUTE_WIRED_HEADSET
802
+ else -> {
803
+ Log.w(TAG, "Unknown audio route: $route")
804
+ return
805
+ }
806
+ }
807
+
808
+ // Set route through active telecom connection
809
+ currentCallId?.let { callId ->
810
+ telecomConnections[callId]?.let { connection ->
811
+ if (connection is MyConnection) {
812
+ connection.setTelecomAudioRoute(telecomRoute)
813
+ Log.d(TAG, "Set telecom audio route to: $telecomRoute for $route")
814
+ }
815
+ }
816
+ }
817
+ }
818
+
819
+ private fun applyAudioRoute(route: String) {
721
820
  val ctx = requireContext()
722
821
  if (audioManager == null) {
723
822
  audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
724
823
  }
725
824
  val am = audioManager!!
726
825
 
826
+ // Ensure we're in the correct audio mode
727
827
  if (am.mode != AudioManager.MODE_IN_COMMUNICATION) {
728
828
  am.mode = AudioManager.MODE_IN_COMMUNICATION
729
829
  }
730
830
 
831
+ val previousRoute = currentAudioRoute
832
+
731
833
  when (route) {
732
834
  "Speaker" -> {
733
835
  am.isSpeakerphoneOn = true
734
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
836
+ if (am.isBluetoothScoOn) {
735
837
  am.stopBluetoothSco()
736
838
  am.isBluetoothScoOn = false
737
839
  }
738
- Log.d(TAG, "Audio routed to SPEAKER")
840
+ currentAudioRoute = "Speaker"
841
+ Log.d(TAG, "Audio route set to SPEAKER")
739
842
  }
740
843
  "Earpiece" -> {
741
844
  am.isSpeakerphoneOn = false
742
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
845
+ if (am.isBluetoothScoOn) {
743
846
  am.stopBluetoothSco()
744
847
  am.isBluetoothScoOn = false
745
848
  }
746
- Log.d(TAG, "Audio routed to EARPIECE")
849
+ currentAudioRoute = "Earpiece"
850
+ Log.d(TAG, "Audio route set to EARPIECE")
747
851
  }
748
852
  "Bluetooth" -> {
749
853
  am.isSpeakerphoneOn = false
750
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
854
+ if (!am.isBluetoothScoOn) {
751
855
  am.startBluetoothSco()
752
856
  am.isBluetoothScoOn = true
753
- Log.d(TAG, "Audio routed to BLUETOOTH")
754
- } else {
755
- Log.w(TAG, "Bluetooth SCO not supported on this OS version")
756
857
  }
858
+ currentAudioRoute = "Bluetooth"
859
+ Log.d(TAG, "Audio route set to BLUETOOTH")
757
860
  }
758
861
  "Headset" -> {
759
862
  am.isSpeakerphoneOn = false
760
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
863
+ if (am.isBluetoothScoOn) {
761
864
  am.stopBluetoothSco()
762
865
  am.isBluetoothScoOn = false
763
866
  }
764
- Log.d(TAG, "Audio routed to HEADSET")
867
+ // For wired headsets, the system automatically routes audio when connected
868
+ currentAudioRoute = "Headset"
869
+ Log.d(TAG, "Audio route set to HEADSET")
765
870
  }
766
871
  else -> {
767
872
  Log.w(TAG, "Unknown audio route: $route")
768
873
  return
769
874
  }
770
875
  }
771
- emitAudioRouteChanged()
876
+
877
+ // Only emit event if route actually changed
878
+ if (currentAudioRoute != previousRoute) {
879
+ emitAudioRouteChanged(currentAudioRoute)
880
+ }
772
881
  }
773
882
 
774
883
  private fun getCurrentAudioRoute(): String {
884
+ val am = audioManager ?: return "Unknown"
885
+
886
+ // Check in order of priority: Bluetooth -> Headset -> Speaker -> Earpiece
775
887
  return when {
776
- audioManager?.isBluetoothScoOn == true -> "Bluetooth"
777
- audioManager?.isSpeakerphoneOn == true -> "Speaker"
778
- audioManager?.isWiredHeadsetOn == true -> "Headset"
888
+ am.isBluetoothScoOn -> "Bluetooth"
889
+ isWiredHeadsetConnected() -> "Headset"
890
+ am.isSpeakerphoneOn -> "Speaker"
779
891
  else -> "Earpiece"
780
892
  }
781
893
  }
782
894
 
895
+ private fun isWiredHeadsetConnected(): Boolean {
896
+ val am = audioManager ?: return false
897
+
898
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
899
+ val devices = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
900
+ return devices.any { device ->
901
+ device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
902
+ device.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES ||
903
+ device.type == AudioDeviceInfo.TYPE_USB_HEADSET
904
+ }
905
+ } else {
906
+ @Suppress("DEPRECATION")
907
+ return am.isWiredHeadsetOn
908
+ }
909
+ }
910
+
911
+ private fun isBluetoothDeviceConnected(): Boolean {
912
+ val am = audioManager ?: return false
913
+
914
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
915
+ val devices = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
916
+ return devices.any { device ->
917
+ device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
918
+ device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
919
+ device.type == AudioDeviceInfo.TYPE_BLE_HEADSET
920
+ }
921
+ } else {
922
+ @Suppress("DEPRECATION")
923
+ return am.isBluetoothA2dpOn || am.isBluetoothScoOn
924
+ }
925
+ }
926
+
783
927
  private fun setInitialAudioRoute(callType: String) {
784
- val avail = getAudioDevices()
785
- // Extract string values for comparison
786
- val deviceStrings = avail.devices.map { it.value }
928
+ Log.d(TAG, "Setting initial audio route for call type: $callType")
929
+
930
+ // Don't override if user manually set a route
931
+ if (wasManuallySet) {
932
+ Log.d(TAG, "Audio route was manually set, skipping initial route")
933
+ return
934
+ }
935
+
936
+ // Determine default route based on Android standards
787
937
  val defaultRoute = when {
788
- deviceStrings.contains("Bluetooth") -> "Bluetooth"
789
- deviceStrings.contains("Headset") -> "Headset"
790
- callType == "Video" -> "Speaker"
791
- else -> "Earpiece"
938
+ isBluetoothDeviceConnected() -> "Bluetooth"
939
+ isWiredHeadsetConnected() -> "Headset"
940
+ callType.equals("Video", ignoreCase = true) -> "Speaker"
941
+ else -> "Earpiece" // Default for audio calls
792
942
  }
793
- Log.d(TAG, "Setting initial audio route: $defaultRoute for call type: $callType")
794
- setAudioRoute(defaultRoute)
943
+
944
+ Log.d(TAG, "Setting initial audio route to: $defaultRoute")
945
+ applyAudioRoute(defaultRoute)
795
946
  }
796
947
 
797
948
  private fun setAudioMode() {
@@ -801,40 +952,124 @@ object CallEngine {
801
952
 
802
953
  private fun resetAudioMode() {
803
954
  if (activeCalls.isEmpty()) {
804
- audioManager?.mode = AudioManager.MODE_NORMAL
805
- audioManager?.stopBluetoothSco()
806
- audioManager?.isBluetoothScoOn = false
807
- audioManager?.isSpeakerphoneOn = false
955
+ audioManager?.let { am ->
956
+ am.mode = AudioManager.MODE_NORMAL
957
+ if (am.isBluetoothScoOn) {
958
+ am.stopBluetoothSco()
959
+ am.isBluetoothScoOn = false
960
+ }
961
+ am.isSpeakerphoneOn = false
962
+ }
963
+ currentAudioRoute = "Earpiece"
964
+ wasManuallySet = false
965
+ unregisterAudioDeviceCallback()
808
966
  Log.d(TAG, "Audio mode reset to MODE_NORMAL")
809
967
  }
810
968
  }
811
969
 
812
- private fun emitAudioRouteChanged() {
813
- val info = getAudioDevices()
814
- // Extract string values from StringHolder objects
815
- val deviceStrings = info.devices.map { it.value }
816
- val payload = JSONObject().apply {
817
- put("devices", JSONArray(deviceStrings))
818
- put("currentRoute", info.currentRoute)
819
- }
820
- emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, payload)
821
- Log.d(TAG, "Audio route changed: ${info.currentRoute}, available: $deviceStrings")
970
+ // UPDATED: Fix the method signature
971
+ private fun emitAudioRouteChanged(currentRoute: String) {
972
+ val info = getAudioDevices()
973
+ val deviceStrings = info.devices.map { it.value }
974
+ val payload = JSONObject().apply {
975
+ put("devices", JSONArray(deviceStrings))
976
+ put("currentRoute", currentRoute)
977
+ }
978
+ emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, payload)
979
+ Log.d(TAG, "Audio route changed: $currentRoute, available: $deviceStrings")
822
980
  }
823
981
 
824
982
  private val audioDeviceCallback = object : AudioDeviceCallback() {
825
983
  override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
826
984
  Log.d(TAG, "Audio devices added")
827
- emitAudioDevicesChanged()
985
+ handleAudioDeviceChange(addedDevices, true)
828
986
  }
987
+
829
988
  override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
830
989
  Log.d(TAG, "Audio devices removed")
831
- emitAudioDevicesChanged()
990
+ handleAudioDeviceChange(removedDevices, false)
832
991
  }
833
992
  }
834
993
 
994
+ private fun handleAudioDeviceChange(devices: Array<out AudioDeviceInfo>?, isAdded: Boolean) {
995
+ if (devices == null || !isCallActive()) return
996
+
997
+ val context = requireContext()
998
+ val currentCallInfo = getCurrentActiveCall()
999
+ if (currentCallInfo == null) {
1000
+ Log.d(TAG, "No active call, ignoring device change")
1001
+ return
1002
+ }
1003
+
1004
+ val relevantDevices = devices.filter { device ->
1005
+ device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
1006
+ device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
1007
+ device.type == AudioDeviceInfo.TYPE_BLE_HEADSET ||
1008
+ device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
1009
+ device.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES ||
1010
+ device.type == AudioDeviceInfo.TYPE_USB_HEADSET
1011
+ }
1012
+
1013
+ if (relevantDevices.isEmpty()) {
1014
+ Log.d(TAG, "No relevant devices in change event")
1015
+ return
1016
+ }
1017
+
1018
+ Log.d(TAG, "Relevant device change detected. Added: $isAdded, wasManuallySet: $wasManuallySet")
1019
+
1020
+ if (isAdded && !wasManuallySet) {
1021
+ // Device connected - switch to it automatically if user hasn't manually set route
1022
+ val deviceType = relevantDevices.first().type
1023
+ val newRoute = when (deviceType) {
1024
+ AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
1025
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
1026
+ AudioDeviceInfo.TYPE_BLE_HEADSET -> "Bluetooth"
1027
+ AudioDeviceInfo.TYPE_WIRED_HEADSET,
1028
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
1029
+ AudioDeviceInfo.TYPE_USB_HEADSET -> "Headset"
1030
+ else -> null
1031
+ }
1032
+
1033
+ if (newRoute != null && newRoute != currentAudioRoute) {
1034
+ Log.d(TAG, "Auto-switching to newly connected device: $newRoute")
1035
+ // Add slight delay to ensure device is ready
1036
+ mainHandler.postDelayed({
1037
+ applyAudioRoute(newRoute)
1038
+ }, 300)
1039
+ }
1040
+ } else if (!isAdded) {
1041
+ // Device disconnected - fall back to appropriate route
1042
+ val disconnectedType = relevantDevices.first().type
1043
+ val wasCurrentRoute = when (disconnectedType) {
1044
+ AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
1045
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
1046
+ AudioDeviceInfo.TYPE_BLE_HEADSET -> currentAudioRoute == "Bluetooth"
1047
+ AudioDeviceInfo.TYPE_WIRED_HEADSET,
1048
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
1049
+ AudioDeviceInfo.TYPE_USB_HEADSET -> currentAudioRoute == "Headset"
1050
+ else -> false
1051
+ }
1052
+
1053
+ if (wasCurrentRoute) {
1054
+ Log.d(TAG, "Current audio device disconnected, falling back")
1055
+ // Reset manual flag since the manually selected device is gone
1056
+ wasManuallySet = false
1057
+ mainHandler.postDelayed({
1058
+ setInitialAudioRoute(currentCallInfo.callType)
1059
+ }, 300)
1060
+ }
1061
+ }
1062
+
1063
+ // Always emit devices changed event
1064
+ emitAudioDevicesChanged()
1065
+ }
1066
+
1067
+ private fun getCurrentActiveCall(): CallInfo? {
1068
+ return activeCalls.values.find { it.state == CallState.ACTIVE }
1069
+ }
1070
+
835
1071
  private fun emitAudioDevicesChanged() {
836
1072
  val info = getAudioDevices()
837
- // Extract string values from StringHolder objects
838
1073
  val deviceStrings = info.devices.map { it.value }
839
1074
  val payload = JSONObject().apply {
840
1075
  put("devices", JSONArray(deviceStrings))
@@ -845,17 +1080,29 @@ object CallEngine {
845
1080
  }
846
1081
 
847
1082
  fun registerAudioDeviceCallback() {
848
- val context = requireContext()
849
- audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
850
- audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
1083
+ if (isCallActive()) {
1084
+ val context = requireContext()
1085
+ audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
1086
+ try {
1087
+ audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
1088
+ Log.d(TAG, "Audio device callback registered")
1089
+ } catch (e: Exception) {
1090
+ Log.w(TAG, "Failed to register audio device callback: ${e.message}")
1091
+ }
1092
+ }
851
1093
  }
852
1094
 
853
1095
  fun unregisterAudioDeviceCallback() {
854
- val context = requireContext()
855
- audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
856
- audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
1096
+ try {
1097
+ audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
1098
+ Log.d(TAG, "Audio device callback unregistered")
1099
+ } catch (e: Exception) {
1100
+ Log.w(TAG, "Failed to unregister audio device callback: ${e.message}")
1101
+ }
857
1102
  }
858
1103
 
1104
+ // ====== END AUDIO ROUTING SYSTEM ======
1105
+
859
1106
  fun keepScreenAwake(keepAwake: Boolean) {
860
1107
  val context = requireContext()
861
1108
  val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
@@ -917,7 +1164,6 @@ object CallEngine {
917
1164
  channel.setBypassDnd(true)
918
1165
  channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
919
1166
 
920
- // NEW: Improved sound handling to prevent double ringing
921
1167
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
922
1168
  channel.setSound(
923
1169
  RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE),
@@ -927,7 +1173,6 @@ object CallEngine {
927
1173
  .build()
928
1174
  )
929
1175
  } else {
930
- // For API 31+, disable notification sound to prevent conflicts with custom ringtone
931
1176
  channel.setSound(null, null)
932
1177
  channel.importance = NotificationManager.IMPORTANCE_HIGH
933
1178
  }
@@ -40,16 +40,11 @@ class MyConnection(
40
40
  Log.d(TAG, "MyConnection for callId $callId created and added to CallEngine. Type: $callType")
41
41
  }
42
42
 
43
- // --- THIS IS THE MISSING PIECE ---
44
- /**
45
- * Called by the system when the user presses a volume key during ringing.
46
- */
47
43
  override fun onSilence() {
48
44
  super.onSilence()
49
45
  Log.d(TAG, "onSilence called by system for callId: $callId. Silencing ringtone.")
50
46
  CallEngine.silenceIncomingCall()
51
47
  }
52
- // ---------------------------------
53
48
 
54
49
  override fun onAnswer() {
55
50
  Log.d(TAG, "Call answered via Telecom for callId: $callId")
@@ -97,6 +92,8 @@ class MyConnection(
97
92
 
98
93
  if (lastAudioState == null || lastAudioState!!.route != state.route) {
99
94
  Log.d(TAG, "System audio route changed for callId: $callId. Telecom route: ${state.route}")
95
+ // Notify CallEngine about the actual route change
96
+ CallEngine.onTelecomAudioRouteChanged(callId, state)
100
97
  }
101
98
 
102
99
  lastAudioState = state
@@ -134,6 +131,8 @@ class MyConnection(
134
131
  }
135
132
  STATE_ACTIVE -> {
136
133
  Log.d(TAG, "Connection is now active for callId: $callId")
134
+ // Set initial audio route when call becomes active
135
+ CallEngine.setInitialAudioRouteForCall(callId, callType)
137
136
  }
138
137
  STATE_DISCONNECTED -> {
139
138
  Log.d(TAG, "Connection is now disconnected for callId: $callId")
@@ -141,4 +140,10 @@ class MyConnection(
141
140
  }
142
141
  }
143
142
  }
143
+
144
+ // NEW: Method to set audio route through telecom
145
+ fun setTelecomAudioRoute(route: Int) {
146
+ Log.d(TAG, "Setting telecom audio route to: $route for callId: $callId")
147
+ setAudioRoute(route)
148
+ }
144
149
  }
@@ -77,6 +77,23 @@ public class CallManager: HybridCallManagerSpec {
77
77
  )
78
78
  }
79
79
 
80
+ public func reportIncomingCall(callId: String,
81
+ callType: String,
82
+ targetName: String,
83
+ metadata: String?) throws
84
+ {
85
+ logger.info("🎯 startOutgoingCall ▶ js → native: \(callId), type=\(callType)")
86
+ if let m = metadata { logger.debug("🎯 metadata.len=\(m.count)") }
87
+ CallEngine.shared.reportIncomingCall(
88
+ callId: callId,
89
+ callType: callType,
90
+ targetName: targetName,
91
+ nil,
92
+ metadata: metadata
93
+ )
94
+ }
95
+
96
+
80
97
  public func startCall(callId: String,
81
98
  callType: String,
82
99
  targetName: String,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.150",
3
+ "version": "0.1.152",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",