@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
|
-
*
|
|
44
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
693
|
-
|
|
694
|
-
when (
|
|
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 ->
|
|
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
|
|
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)
|
|
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
|
-
|
|
719
|
-
|
|
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 (
|
|
836
|
+
if (am.isBluetoothScoOn) {
|
|
735
837
|
am.stopBluetoothSco()
|
|
736
838
|
am.isBluetoothScoOn = false
|
|
737
839
|
}
|
|
738
|
-
|
|
840
|
+
currentAudioRoute = "Speaker"
|
|
841
|
+
Log.d(TAG, "Audio route set to SPEAKER")
|
|
739
842
|
}
|
|
740
843
|
"Earpiece" -> {
|
|
741
844
|
am.isSpeakerphoneOn = false
|
|
742
|
-
if (
|
|
845
|
+
if (am.isBluetoothScoOn) {
|
|
743
846
|
am.stopBluetoothSco()
|
|
744
847
|
am.isBluetoothScoOn = false
|
|
745
848
|
}
|
|
746
|
-
|
|
849
|
+
currentAudioRoute = "Earpiece"
|
|
850
|
+
Log.d(TAG, "Audio route set to EARPIECE")
|
|
747
851
|
}
|
|
748
852
|
"Bluetooth" -> {
|
|
749
853
|
am.isSpeakerphoneOn = false
|
|
750
|
-
if (
|
|
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 (
|
|
863
|
+
if (am.isBluetoothScoOn) {
|
|
761
864
|
am.stopBluetoothSco()
|
|
762
865
|
am.isBluetoothScoOn = false
|
|
763
866
|
}
|
|
764
|
-
|
|
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
|
-
|
|
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
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
callType
|
|
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
|
-
|
|
794
|
-
|
|
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?.
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
}
|
package/ios/CallManager.swift
CHANGED
|
@@ -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,
|