@qusaieilouti99/call-manager 0.1.170 → 0.1.172
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.
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/CallEngine.kt +192 -505
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/MyConnection.kt +94 -73
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/MyConnectionService.kt +9 -9
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.margelo.nitro.qusaieilouti99.callmanager
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import android.app.ActivityManager
|
|
4
|
+
import android.app.KeyguardManager
|
|
4
5
|
import android.app.Notification
|
|
5
6
|
import android.app.NotificationChannel
|
|
6
7
|
import android.app.NotificationManager
|
|
@@ -21,6 +22,10 @@ import android.os.Bundle
|
|
|
21
22
|
import android.os.Handler
|
|
22
23
|
import android.os.Looper
|
|
23
24
|
import android.os.PowerManager
|
|
25
|
+
import android.os.VibrationEffect
|
|
26
|
+
import android.os.Vibrator
|
|
27
|
+
import android.telecom.CallAudioState
|
|
28
|
+
import android.telecom.CallEndpoint
|
|
24
29
|
import android.telecom.Connection
|
|
25
30
|
import android.telecom.DisconnectCause
|
|
26
31
|
import android.telecom.PhoneAccount
|
|
@@ -32,9 +37,6 @@ import org.json.JSONObject
|
|
|
32
37
|
import java.util.concurrent.ConcurrentHashMap
|
|
33
38
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
34
39
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
35
|
-
import android.app.KeyguardManager
|
|
36
|
-
import android.os.Vibrator
|
|
37
|
-
import android.os.VibrationEffect
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* Core call‐management engine. Manages self-managed telecom calls,
|
|
@@ -88,10 +90,8 @@ object CallEngine {
|
|
|
88
90
|
private var eventHandler: ((CallEventType, String) -> Unit)? = null
|
|
89
91
|
private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
|
|
90
92
|
|
|
91
|
-
// Audio routing state
|
|
92
|
-
|
|
93
|
-
private var wasManuallySet: Boolean = false
|
|
94
|
-
private var callStartTime: Long = 0
|
|
93
|
+
// Audio routing state no longer directly managed here, but we keep AudioManager
|
|
94
|
+
// instance for mute control and mode settings.
|
|
95
95
|
|
|
96
96
|
interface LockScreenBypassCallback {
|
|
97
97
|
fun onLockScreenBypassChanged(shouldBypass: Boolean)
|
|
@@ -325,7 +325,7 @@ object CallEngine {
|
|
|
325
325
|
currentCallId = callId
|
|
326
326
|
Log.d(TAG, "Call $callId added to activeCalls. State: DIALING")
|
|
327
327
|
|
|
328
|
-
setAudioMode()
|
|
328
|
+
setAudioMode() // Set audio mode early
|
|
329
329
|
registerPhoneAccount()
|
|
330
330
|
|
|
331
331
|
val telecomManager =
|
|
@@ -344,6 +344,7 @@ object CallEngine {
|
|
|
344
344
|
val extras = Bundle().apply {
|
|
345
345
|
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
|
|
346
346
|
putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
|
|
347
|
+
// Pass the speakerphone hint to Telecom
|
|
347
348
|
putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, isVideoCall)
|
|
348
349
|
}
|
|
349
350
|
|
|
@@ -363,96 +364,47 @@ object CallEngine {
|
|
|
363
364
|
}
|
|
364
365
|
|
|
365
366
|
fun startCall(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
callId: String,
|
|
368
|
+
callType: String,
|
|
369
|
+
targetName: String,
|
|
370
|
+
metadata: String? = null
|
|
370
371
|
) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
// Check if this is an existing incoming call that should be answered
|
|
375
|
-
val existingCall = activeCalls[callId]
|
|
376
|
-
if (existingCall != null && existingCall.state == CallState.INCOMING) {
|
|
377
|
-
Log.d(TAG, "startCall for existing incoming call - answering: $callId")
|
|
378
|
-
answerCall(callId)
|
|
379
|
-
return
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// If call doesn't exist, create it as a direct active call (like conference join)
|
|
383
|
-
if (existingCall == null) {
|
|
384
|
-
Log.d(TAG, "startCall creating new direct active call: $callId")
|
|
385
|
-
|
|
386
|
-
if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
|
|
387
|
-
activeCalls.values.forEach {
|
|
388
|
-
if (it.state == CallState.ACTIVE) {
|
|
389
|
-
holdCallInternal(it.callId, heldBySystem = false)
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
|
|
395
|
-
currentCallId = callId
|
|
396
|
-
callStartTime = System.currentTimeMillis()
|
|
397
|
-
wasManuallySet = false
|
|
398
|
-
Log.d(TAG, "Call $callId started as ACTIVE")
|
|
399
|
-
|
|
400
|
-
registerPhoneAccount()
|
|
401
|
-
setAudioMode()
|
|
402
|
-
bringAppToForeground()
|
|
403
|
-
startForegroundService()
|
|
404
|
-
keepScreenAwake(true)
|
|
372
|
+
Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
|
|
373
|
+
metadata?.let { callMetadata[callId] = it }
|
|
405
374
|
|
|
406
|
-
|
|
407
|
-
|
|
375
|
+
if (activeCalls.containsKey(callId)) {
|
|
376
|
+
Log.w(TAG, "Call $callId already exists, cannot start again")
|
|
377
|
+
return
|
|
378
|
+
}
|
|
408
379
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
380
|
+
if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
|
|
381
|
+
activeCalls.values.forEach {
|
|
382
|
+
if (it.state == CallState.ACTIVE) {
|
|
383
|
+
holdCallInternal(it.callId, heldBySystem = false)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
413
387
|
|
|
414
|
-
|
|
415
|
-
|
|
388
|
+
activeCalls[callId] =
|
|
389
|
+
CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
|
|
390
|
+
currentCallId = callId
|
|
391
|
+
// callStartTime = System.currentTimeMillis() // CallInfo handles timestamp
|
|
392
|
+
Log.d(TAG, "Call $callId started as ACTIVE")
|
|
416
393
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
394
|
+
registerPhoneAccount()
|
|
395
|
+
setAudioMode() // Set audio mode early
|
|
396
|
+
bringAppToForeground()
|
|
397
|
+
startForegroundService()
|
|
398
|
+
keepScreenAwake(true)
|
|
423
399
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
callType: String,
|
|
427
|
-
targetName: String,
|
|
428
|
-
metadata: String?
|
|
429
|
-
) {
|
|
430
|
-
val context = requireContext()
|
|
431
|
-
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
432
|
-
val phoneAccountHandle = getPhoneAccountHandle()
|
|
433
|
-
val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
|
|
434
|
-
val isVideoCall = callType == "Video"
|
|
435
|
-
|
|
436
|
-
val outgoingExtras = Bundle().apply {
|
|
437
|
-
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
438
|
-
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
439
|
-
putString(MyConnectionService.EXTRA_DISPLAY_NAME, targetName)
|
|
440
|
-
putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, isVideoCall)
|
|
441
|
-
metadata?.let { putString("metadata", it) }
|
|
442
|
-
}
|
|
400
|
+
// Register audio device callback to handle dynamic device changes (only for event emission)
|
|
401
|
+
registerAudioDeviceCallback()
|
|
443
402
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
|
|
447
|
-
putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, isVideoCall)
|
|
448
|
-
}
|
|
403
|
+
// The initial audio route setting will be handled by MyConnection
|
|
404
|
+
// when it receives STATE_ACTIVE callback from Telecom.
|
|
449
405
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
Log.d(TAG, "Successfully reported startCall to TelecomManager")
|
|
453
|
-
} catch (e: Exception) {
|
|
454
|
-
Log.e(TAG, "Failed to report startCall to telecom: ${e.message}", e)
|
|
455
|
-
}
|
|
406
|
+
updateLockScreenBypass()
|
|
407
|
+
emitOutgoingCallAnsweredWithMetadata(callId) // Assuming this is for a call that's immediately active, not dialing.
|
|
456
408
|
}
|
|
457
409
|
|
|
458
410
|
fun callAnsweredFromJS(callId: String) {
|
|
@@ -475,8 +427,7 @@ object CallEngine {
|
|
|
475
427
|
|
|
476
428
|
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
477
429
|
currentCallId = callId
|
|
478
|
-
callStartTime = System.currentTimeMillis()
|
|
479
|
-
wasManuallySet = false
|
|
430
|
+
// callStartTime = System.currentTimeMillis() // CallInfo handles timestamp
|
|
480
431
|
Log.d(TAG, "Call $callId set to ACTIVE state")
|
|
481
432
|
|
|
482
433
|
stopRingtone()
|
|
@@ -496,15 +447,13 @@ object CallEngine {
|
|
|
496
447
|
keepScreenAwake(true)
|
|
497
448
|
updateLockScreenBypass()
|
|
498
449
|
|
|
499
|
-
setAudioMode()
|
|
450
|
+
setAudioMode() // Ensure communication audio mode
|
|
500
451
|
|
|
501
|
-
// Register audio device callback to handle dynamic device changes
|
|
452
|
+
// Register audio device callback to handle dynamic device changes (only for event emission)
|
|
502
453
|
registerAudioDeviceCallback()
|
|
503
454
|
|
|
504
|
-
//
|
|
505
|
-
|
|
506
|
-
setInitialAudioRoute(callInfo.callType)
|
|
507
|
-
}, 800L)
|
|
455
|
+
// MyConnection.onStateChanged(STATE_ACTIVE) will trigger setInitialAudioRouteForCall
|
|
456
|
+
// which delegates to MyConnection's method to interact with Telecom.
|
|
508
457
|
|
|
509
458
|
if (isLocalAnswer) {
|
|
510
459
|
emitCallAnsweredWithMetadata(callId)
|
|
@@ -585,7 +534,7 @@ object CallEngine {
|
|
|
585
534
|
wasHeldBySystem = heldBySystem
|
|
586
535
|
)
|
|
587
536
|
|
|
588
|
-
telecomConnections[callId]?.setOnHold()
|
|
537
|
+
telecomConnections[callId]?.setOnHold() // Request Telecom to hold
|
|
589
538
|
updateForegroundNotification()
|
|
590
539
|
emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
|
|
591
540
|
updateLockScreenBypass()
|
|
@@ -608,7 +557,7 @@ object CallEngine {
|
|
|
608
557
|
wasHeldBySystem = false
|
|
609
558
|
)
|
|
610
559
|
|
|
611
|
-
telecomConnections[callId]?.setActive()
|
|
560
|
+
telecomConnections[callId]?.setActive() // Request Telecom to unhold/set active
|
|
612
561
|
updateForegroundNotification()
|
|
613
562
|
emitEvent(CallEventType.CALL_UNHELD, JSONObject().put("callId", callId))
|
|
614
563
|
updateLockScreenBypass()
|
|
@@ -626,23 +575,21 @@ object CallEngine {
|
|
|
626
575
|
setMutedInternal(callId, muted)
|
|
627
576
|
}
|
|
628
577
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
return
|
|
634
|
-
}
|
|
635
|
-
|
|
578
|
+
fun setMutedInternal(callId: String, muted: Boolean) {
|
|
579
|
+
// This is called by MyConnection.onMuteStateChanged (API 34+)
|
|
580
|
+
// or MyConnection.onCallAudioStateChanged (pre-API 34)
|
|
581
|
+
// or directly from CallManager JS calls.
|
|
636
582
|
val context = requireContext()
|
|
637
583
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
638
584
|
|
|
639
585
|
val wasMuted = audioManager?.isMicrophoneMute ?: false
|
|
640
|
-
audioManager?.isMicrophoneMute = muted
|
|
641
|
-
|
|
642
586
|
if (wasMuted != muted) {
|
|
587
|
+
audioManager?.isMicrophoneMute = muted
|
|
643
588
|
val eventType = if (muted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
|
|
644
589
|
emitEvent(eventType, JSONObject().put("callId", callId))
|
|
645
|
-
Log.d(TAG, "Call $callId mute state changed to: $muted")
|
|
590
|
+
Log.d(TAG, "Call $callId microphone mute state changed to: $muted")
|
|
591
|
+
} else {
|
|
592
|
+
Log.d(TAG, "Call $callId microphone mute state already $muted, no change.")
|
|
646
593
|
}
|
|
647
594
|
}
|
|
648
595
|
|
|
@@ -679,8 +626,8 @@ object CallEngine {
|
|
|
679
626
|
val metadata = callMetadata.remove(callId)
|
|
680
627
|
activeCalls.remove(callId)
|
|
681
628
|
|
|
682
|
-
stopRingback()
|
|
683
629
|
stopRingtone()
|
|
630
|
+
stopRingback()
|
|
684
631
|
cancelIncomingCallUI()
|
|
685
632
|
|
|
686
633
|
if (currentCallId == callId) {
|
|
@@ -735,330 +682,146 @@ object CallEngine {
|
|
|
735
682
|
|
|
736
683
|
// ====== IMPROVED AUDIO ROUTING SYSTEM ======
|
|
737
684
|
|
|
738
|
-
|
|
739
|
-
fun getAudioDevices(): AudioRoutesInfo {
|
|
685
|
+
fun getAudioDevices(): AudioRoutesInfo {
|
|
740
686
|
val context = requireContext()
|
|
741
687
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
742
|
-
|
|
688
|
+
?: return AudioRoutesInfo(emptyArray(), "Unknown")
|
|
743
689
|
|
|
744
690
|
val devices = mutableSetOf<String>()
|
|
745
|
-
var
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
|
|
764
|
-
AudioDeviceInfo.TYPE_WIRED_HEADSET,
|
|
765
|
-
AudioDeviceInfo.TYPE_USB_HEADSET -> {
|
|
766
|
-
if (!hasWiredHeadset) {
|
|
767
|
-
devices.add("Headset")
|
|
768
|
-
hasWiredHeadset = true
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
AudioDeviceInfo.TYPE_BLE_HEADSET -> {
|
|
772
|
-
if (!hasBluetoothDevice) {
|
|
773
|
-
devices.add("Bluetooth")
|
|
774
|
-
hasBluetoothDevice = true
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
} else {
|
|
780
|
-
// Fallback for older API levels
|
|
781
|
-
@Suppress("DEPRECATION")
|
|
782
|
-
if (audioManager?.isBluetoothA2dpOn == true || audioManager?.isBluetoothScoOn == true) {
|
|
783
|
-
devices.add("Bluetooth")
|
|
784
|
-
hasBluetoothDevice = true
|
|
785
|
-
}
|
|
786
|
-
@Suppress("DEPRECATION")
|
|
787
|
-
if (audioManager?.isWiredHeadsetOn == true) {
|
|
788
|
-
devices.add("Headset")
|
|
789
|
-
hasWiredHeadset = true
|
|
691
|
+
var currentRouteString: String = "Unknown"
|
|
692
|
+
|
|
693
|
+
val currentCallConnection = currentCallId?.let { telecomConnections[it] as? MyConnection }
|
|
694
|
+
|
|
695
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // API 34+
|
|
696
|
+
// For API 34+, prefer getting actual Telecom CallEndpoint info from MyConnection
|
|
697
|
+
currentCallConnection?.let { conn ->
|
|
698
|
+
val currentEndpoint = conn.getCurrentTelecomCallEndpoint()
|
|
699
|
+
currentEndpoint?.let {
|
|
700
|
+
currentRouteString = when (it.getEndpointType()) {
|
|
701
|
+
CallEndpoint.TYPE_EARPIECE -> "Earpiece"
|
|
702
|
+
CallEndpoint.TYPE_SPEAKER -> "Speaker"
|
|
703
|
+
CallEndpoint.TYPE_BLUETOOTH -> "Bluetooth"
|
|
704
|
+
CallEndpoint.TYPE_WIRED_HEADSET -> "Headset"
|
|
705
|
+
else -> "Unknown"
|
|
706
|
+
}
|
|
790
707
|
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
val current = getCurrentAudioRoute()
|
|
794
|
-
Log.d(TAG, "Available audio devices: ${devices.toList()}, current: $current")
|
|
795
708
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
CallEndpoint.TYPE_SPEAKER -> "Speaker"
|
|
807
|
-
CallEndpoint.TYPE_BLUETOOTH -> "Bluetooth"
|
|
808
|
-
CallEndpoint.TYPE_WIRED_HEADSET -> "Headset"
|
|
809
|
-
else -> "Unknown"
|
|
709
|
+
val availableEndpoints = conn.getAvailableTelecomCallEndpoints()
|
|
710
|
+
availableEndpoints.forEach { endpoint ->
|
|
711
|
+
when (endpoint.getEndpointType()) {
|
|
712
|
+
CallEndpoint.TYPE_EARPIECE -> devices.add("Earpiece")
|
|
713
|
+
CallEndpoint.TYPE_SPEAKER -> devices.add("Speaker")
|
|
714
|
+
CallEndpoint.TYPE_BLUETOOTH -> devices.add("Bluetooth")
|
|
715
|
+
CallEndpoint.TYPE_WIRED_HEADSET -> devices.add("Headset")
|
|
716
|
+
// TYPE_STREAMING, TYPE_UNKNOWN are not typically user selectable audio routes
|
|
717
|
+
}
|
|
718
|
+
}
|
|
810
719
|
}
|
|
811
720
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
721
|
+
// If no active call connection or API < 34, fallback to traditional device checks
|
|
722
|
+
if (devices.isEmpty()) {
|
|
723
|
+
devices.add("Earpiece")
|
|
724
|
+
devices.add("Speaker")
|
|
725
|
+
if (isBluetoothDeviceConnectedLegacy()) devices.add("Bluetooth")
|
|
726
|
+
if (isWiredHeadsetConnectedLegacy()) devices.add("Headset")
|
|
727
|
+
}
|
|
728
|
+
if (currentRouteString == "Unknown") {
|
|
729
|
+
currentRouteString = getCurrentAudioRouteLegacy()
|
|
730
|
+
}
|
|
820
731
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
}
|
|
829
|
-
}.distinct()
|
|
732
|
+
} else { // Pre API 34
|
|
733
|
+
devices.add("Earpiece")
|
|
734
|
+
devices.add("Speaker")
|
|
735
|
+
if (isBluetoothDeviceConnectedLegacy()) devices.add("Bluetooth")
|
|
736
|
+
if (isWiredHeadsetConnectedLegacy()) devices.add("Headset")
|
|
737
|
+
currentRouteString = getCurrentAudioRouteLegacy()
|
|
738
|
+
}
|
|
830
739
|
|
|
831
|
-
|
|
832
|
-
|
|
740
|
+
Log.d(TAG, "Available audio devices: ${devices.toList()}, current: $currentRouteString")
|
|
741
|
+
val deviceHolders = devices.map { StringHolder(it) }.toTypedArray()
|
|
742
|
+
return AudioRoutesInfo(deviceHolders, currentRouteString)
|
|
833
743
|
}
|
|
834
744
|
|
|
835
|
-
|
|
745
|
+
/**
|
|
746
|
+
* Called by MyConnection when Telecom reports an audio route change.
|
|
747
|
+
* This method translates the route to string and emits to JS.
|
|
748
|
+
* For API 34+, onCallEndpointChanged in MyConnection is the primary way to get route changes.
|
|
749
|
+
* For pre-API 34, onCallAudioStateChanged in MyConnection reports via CallAudioState.
|
|
750
|
+
*/
|
|
836
751
|
fun onTelecomAudioRouteChanged(callId: String, audioState: CallAudioState) {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
CallAudioState.ROUTE_BLUETOOTH -> "Bluetooth"
|
|
847
|
-
CallAudioState.ROUTE_WIRED_HEADSET -> "Headset"
|
|
848
|
-
else -> "Unknown"
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
currentAudioRoute = routeString
|
|
852
|
-
Log.d(TAG, "Emitting AUDIO_ROUTE_CHANGED: currentRoute=$routeString")
|
|
853
|
-
emitAudioRouteChanged(routeString)
|
|
752
|
+
Log.d(TAG, "onTelecomAudioRouteChanged (deprecated callback for API < 34): route=${audioState.route}")
|
|
753
|
+
val routeString = when (audioState.route) {
|
|
754
|
+
CallAudioState.ROUTE_EARPIECE -> "Earpiece"
|
|
755
|
+
CallAudioState.ROUTE_SPEAKER -> "Speaker"
|
|
756
|
+
CallAudioState.ROUTE_BLUETOOTH -> "Bluetooth"
|
|
757
|
+
CallAudioState.ROUTE_WIRED_HEADSET -> "Headset"
|
|
758
|
+
else -> "Unknown"
|
|
759
|
+
}
|
|
760
|
+
emitAudioRouteChanged(routeString) // Emit based on route provided by Telecom
|
|
854
761
|
}
|
|
855
|
-
|
|
762
|
+
|
|
763
|
+
// NEW: Set initial audio route using telecom's connection method.
|
|
764
|
+
// This is called by MyConnection.onStateChanged(STATE_ACTIVE).
|
|
856
765
|
fun setInitialAudioRouteForCall(callId: String, callType: String) {
|
|
857
766
|
Log.d(TAG, "Setting initial audio route for $callId, type: $callType")
|
|
858
767
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
Log.d(TAG, "Audio route was manually set, skipping initial route")
|
|
768
|
+
val connection = telecomConnections[callId] as? MyConnection ?: run {
|
|
769
|
+
Log.w(TAG, "MyConnection not found for callId: $callId, cannot set initial audio route.")
|
|
862
770
|
return
|
|
863
771
|
}
|
|
864
772
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
} else {
|
|
869
|
-
setInitialAudioRouteLegacy(callId, callType)
|
|
870
|
-
}
|
|
871
|
-
}, 200)
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
875
|
-
private fun setInitialAudioRouteModern(callId: String, callType: String) {
|
|
876
|
-
Log.d(TAG, "Setting initial audio route (modern) for $callId, type: $callType")
|
|
877
|
-
|
|
878
|
-
telecomConnections[callId]?.let { connection ->
|
|
879
|
-
if (connection is MyConnection) {
|
|
880
|
-
val currentEndpoint = connection.getCurrentCallEndpoint()
|
|
881
|
-
Log.d(TAG, "Current endpoint: ${currentEndpoint?.endpointName}, type: ${currentEndpoint?.endpointType}")
|
|
882
|
-
|
|
883
|
-
// Determine desired route based on call type and available devices
|
|
884
|
-
val isVideoCall = callType.equals("Video", ignoreCase = true)
|
|
885
|
-
val desiredEndpointType = when {
|
|
886
|
-
isBluetoothDeviceConnected() -> CallEndpoint.TYPE_BLUETOOTH
|
|
887
|
-
isWiredHeadsetConnected() -> CallEndpoint.TYPE_WIRED_HEADSET
|
|
888
|
-
isVideoCall -> CallEndpoint.TYPE_SPEAKER
|
|
889
|
-
else -> CallEndpoint.TYPE_EARPIECE
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// Only change if current endpoint is different from desired
|
|
893
|
-
if (currentEndpoint == null || currentEndpoint.endpointType != desiredEndpointType) {
|
|
894
|
-
// Create desired endpoint (this is a simplified approach)
|
|
895
|
-
// In reality, you should use the endpoints from onAvailableCallEndpointsChanged
|
|
896
|
-
val desiredEndpoint = createCallEndpoint(desiredEndpointType)
|
|
897
|
-
connection.requestTelecomAudioRouteChange(desiredEndpoint)
|
|
898
|
-
Log.d(TAG, "Requested audio route change to type: $desiredEndpointType")
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
private fun setInitialAudioRouteLegacy(callId: String, callType: String) {
|
|
905
|
-
Log.d(TAG, "Setting initial audio route (legacy) for $callId, type: $callType")
|
|
906
|
-
|
|
907
|
-
val desiredRoute = when {
|
|
908
|
-
isBluetoothDeviceConnected() -> CallAudioState.ROUTE_BLUETOOTH
|
|
909
|
-
isWiredHeadsetConnected() -> CallAudioState.ROUTE_WIRED_HEADSET
|
|
773
|
+
val desiredRouteTelecomInt = when {
|
|
774
|
+
isBluetoothDeviceConnectedLegacy() -> CallAudioState.ROUTE_BLUETOOTH
|
|
775
|
+
isWiredHeadsetConnectedLegacy() -> CallAudioState.ROUTE_WIRED_HEADSET
|
|
910
776
|
callType.equals("Video", ignoreCase = true) -> CallAudioState.ROUTE_SPEAKER
|
|
911
777
|
else -> CallAudioState.ROUTE_EARPIECE
|
|
912
778
|
}
|
|
913
779
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
connection.setAudioRoute(desiredRoute)
|
|
918
|
-
Log.d(TAG, "Set legacy audio route to: $desiredRoute")
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
924
|
-
private fun createCallEndpoint(endpointType: Int): CallEndpoint {
|
|
925
|
-
val name = when (endpointType) {
|
|
926
|
-
CallEndpoint.TYPE_EARPIECE -> "Earpiece"
|
|
927
|
-
CallEndpoint.TYPE_SPEAKER -> "Speaker"
|
|
928
|
-
CallEndpoint.TYPE_BLUETOOTH -> "Bluetooth"
|
|
929
|
-
CallEndpoint.TYPE_WIRED_HEADSET -> "Headset"
|
|
930
|
-
else -> "Unknown"
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
return CallEndpoint(
|
|
934
|
-
name,
|
|
935
|
-
endpointType,
|
|
936
|
-
android.os.ParcelUuid.randomUUID()
|
|
937
|
-
)
|
|
780
|
+
// Delegate to MyConnection to handle routing via Telecom APIs (new or deprecated based on API level)
|
|
781
|
+
connection.setTelecomAudioRoute(desiredRouteTelecomInt)
|
|
782
|
+
Log.d(TAG, "Requested initial telecom audio route for $callId to: $desiredRouteTelecomInt")
|
|
938
783
|
}
|
|
939
784
|
|
|
940
|
-
|
|
941
|
-
|
|
785
|
+
// UPDATED: Use MyConnection's method to handle manual route changes via Telecom
|
|
786
|
+
fun setAudioRoute(route: String) {
|
|
942
787
|
Log.d(TAG, "setAudioRoute called: $route (manual)")
|
|
943
|
-
|
|
788
|
+
|
|
789
|
+
val telecomRouteInt = when (route) {
|
|
790
|
+
"Speaker" -> CallAudioState.ROUTE_SPEAKER
|
|
791
|
+
"Earpiece" -> CallAudioState.ROUTE_EARPIECE
|
|
792
|
+
"Bluetooth" -> CallAudioState.ROUTE_BLUETOOTH
|
|
793
|
+
"Headset" -> CallAudioState.ROUTE_WIRED_HEADSET
|
|
794
|
+
else -> {
|
|
795
|
+
Log.w(TAG, "Unknown audio route: $route")
|
|
796
|
+
return
|
|
797
|
+
}
|
|
798
|
+
}
|
|
944
799
|
|
|
945
800
|
currentCallId?.let { callId ->
|
|
946
801
|
telecomConnections[callId]?.let { connection ->
|
|
947
802
|
if (connection is MyConnection) {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
"Bluetooth" -> CallEndpoint.TYPE_BLUETOOTH
|
|
953
|
-
"Headset" -> CallEndpoint.TYPE_WIRED_HEADSET
|
|
954
|
-
else -> {
|
|
955
|
-
Log.w(TAG, "Unknown audio route: $route")
|
|
956
|
-
return
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
val endpoint = createCallEndpoint(endpointType)
|
|
960
|
-
connection.requestTelecomAudioRouteChange(endpoint)
|
|
961
|
-
} else {
|
|
962
|
-
val telecomRoute = when (route) {
|
|
963
|
-
"Speaker" -> CallAudioState.ROUTE_SPEAKER
|
|
964
|
-
"Earpiece" -> CallAudioState.ROUTE_EARPIECE
|
|
965
|
-
"Bluetooth" -> CallAudioState.ROUTE_BLUETOOTH
|
|
966
|
-
"Headset" -> CallAudioState.ROUTE_WIRED_HEADSET
|
|
967
|
-
else -> {
|
|
968
|
-
Log.w(TAG, "Unknown audio route: $route")
|
|
969
|
-
return
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
@Suppress("DEPRECATION")
|
|
973
|
-
connection.setAudioRoute(telecomRoute)
|
|
974
|
-
}
|
|
975
|
-
Log.d(TAG, "Requested audio route change to: $route")
|
|
803
|
+
connection.setTelecomAudioRoute(telecomRouteInt)
|
|
804
|
+
Log.d(TAG, "Set telecom audio route to: $telecomRouteInt for $route (via MyConnection)")
|
|
805
|
+
} else {
|
|
806
|
+
Log.w(TAG, "No MyConnection found for $callId or wrong type for manual audio route.")
|
|
976
807
|
}
|
|
977
|
-
}
|
|
978
|
-
}
|
|
808
|
+
} ?: Log.w(TAG, "No current call ID set for manual audio route.")
|
|
809
|
+
} ?: Log.w(TAG, "No current call ID set for manual audio route.")
|
|
979
810
|
}
|
|
980
811
|
|
|
981
|
-
private fun
|
|
982
|
-
val
|
|
983
|
-
if (audioManager == null) {
|
|
984
|
-
audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
985
|
-
}
|
|
986
|
-
val am = audioManager!!
|
|
987
|
-
|
|
988
|
-
// Ensure we're in the correct audio mode
|
|
989
|
-
if (am.mode != AudioManager.MODE_IN_COMMUNICATION) {
|
|
990
|
-
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
val previousRoute = currentAudioRoute
|
|
994
|
-
|
|
995
|
-
when (route) {
|
|
996
|
-
"Speaker" -> {
|
|
997
|
-
am.isSpeakerphoneOn = true
|
|
998
|
-
if (am.isBluetoothScoOn) {
|
|
999
|
-
am.stopBluetoothSco()
|
|
1000
|
-
am.isBluetoothScoOn = false
|
|
1001
|
-
}
|
|
1002
|
-
currentAudioRoute = "Speaker"
|
|
1003
|
-
Log.d(TAG, "Audio route set to SPEAKER")
|
|
1004
|
-
}
|
|
1005
|
-
"Earpiece" -> {
|
|
1006
|
-
am.isSpeakerphoneOn = false
|
|
1007
|
-
if (am.isBluetoothScoOn) {
|
|
1008
|
-
am.stopBluetoothSco()
|
|
1009
|
-
am.isBluetoothScoOn = false
|
|
1010
|
-
}
|
|
1011
|
-
currentAudioRoute = "Earpiece"
|
|
1012
|
-
Log.d(TAG, "Audio route set to EARPIECE")
|
|
1013
|
-
}
|
|
1014
|
-
"Bluetooth" -> {
|
|
1015
|
-
am.isSpeakerphoneOn = false
|
|
1016
|
-
if (!am.isBluetoothScoOn) {
|
|
1017
|
-
am.startBluetoothSco()
|
|
1018
|
-
am.isBluetoothScoOn = true
|
|
1019
|
-
}
|
|
1020
|
-
currentAudioRoute = "Bluetooth"
|
|
1021
|
-
Log.d(TAG, "Audio route set to BLUETOOTH")
|
|
1022
|
-
}
|
|
1023
|
-
"Headset" -> {
|
|
1024
|
-
am.isSpeakerphoneOn = false
|
|
1025
|
-
if (am.isBluetoothScoOn) {
|
|
1026
|
-
am.stopBluetoothSco()
|
|
1027
|
-
am.isBluetoothScoOn = false
|
|
1028
|
-
}
|
|
1029
|
-
// For wired headsets, the system automatically routes audio when connected
|
|
1030
|
-
currentAudioRoute = "Headset"
|
|
1031
|
-
Log.d(TAG, "Audio route set to HEADSET")
|
|
1032
|
-
}
|
|
1033
|
-
else -> {
|
|
1034
|
-
Log.w(TAG, "Unknown audio route: $route")
|
|
1035
|
-
return
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
812
|
+
private fun getCurrentAudioRouteLegacy(): String {
|
|
813
|
+
val am = audioManager ?: return "Unknown"
|
|
1038
814
|
|
|
1039
|
-
//
|
|
1040
|
-
|
|
1041
|
-
|
|
815
|
+
// Check in order of priority: Bluetooth -> Headset -> Speaker -> Earpiece
|
|
816
|
+
return when {
|
|
817
|
+
am.isBluetoothScoOn -> "Bluetooth"
|
|
818
|
+
isWiredHeadsetConnectedLegacy() -> "Headset"
|
|
819
|
+
am.isSpeakerphoneOn -> "Speaker"
|
|
820
|
+
else -> "Earpiece"
|
|
1042
821
|
}
|
|
1043
822
|
}
|
|
1044
823
|
|
|
1045
|
-
private fun
|
|
1046
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
1047
|
-
// For modern API, we rely on the stored currentAudioRoute from callbacks
|
|
1048
|
-
return currentAudioRoute
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// Legacy detection
|
|
1052
|
-
val am = audioManager ?: return "Unknown"
|
|
1053
|
-
return when {
|
|
1054
|
-
am.isBluetoothScoOn -> "Bluetooth"
|
|
1055
|
-
isWiredHeadsetConnected() -> "Headset"
|
|
1056
|
-
am.isSpeakerphoneOn -> "Speaker"
|
|
1057
|
-
else -> "Earpiece"
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
private fun isWiredHeadsetConnected(): Boolean {
|
|
824
|
+
private fun isWiredHeadsetConnectedLegacy(): Boolean {
|
|
1062
825
|
val am = audioManager ?: return false
|
|
1063
826
|
|
|
1064
827
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
@@ -1074,7 +837,7 @@ fun getAudioDevices(): AudioRoutesInfo {
|
|
|
1074
837
|
}
|
|
1075
838
|
}
|
|
1076
839
|
|
|
1077
|
-
private fun
|
|
840
|
+
private fun isBluetoothDeviceConnectedLegacy(): Boolean {
|
|
1078
841
|
val am = audioManager ?: return false
|
|
1079
842
|
|
|
1080
843
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
@@ -1090,27 +853,6 @@ fun getAudioDevices(): AudioRoutesInfo {
|
|
|
1090
853
|
}
|
|
1091
854
|
}
|
|
1092
855
|
|
|
1093
|
-
private fun setInitialAudioRoute(callType: String) {
|
|
1094
|
-
Log.d(TAG, "Setting initial audio route for call type: $callType")
|
|
1095
|
-
|
|
1096
|
-
// Don't override if user manually set a route
|
|
1097
|
-
if (wasManuallySet) {
|
|
1098
|
-
Log.d(TAG, "Audio route was manually set, skipping initial route")
|
|
1099
|
-
return
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// Determine default route based on Android standards
|
|
1103
|
-
val defaultRoute = when {
|
|
1104
|
-
isBluetoothDeviceConnected() -> "Bluetooth"
|
|
1105
|
-
isWiredHeadsetConnected() -> "Headset"
|
|
1106
|
-
callType.equals("Video", ignoreCase = true) -> "Speaker"
|
|
1107
|
-
else -> "Earpiece" // Default for audio calls
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
Log.d(TAG, "Setting initial audio route to: $defaultRoute")
|
|
1111
|
-
applyAudioRoute(defaultRoute)
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
856
|
private fun setAudioMode() {
|
|
1115
857
|
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
1116
858
|
Log.d(TAG, "Audio mode set to MODE_IN_COMMUNICATION")
|
|
@@ -1126,16 +868,16 @@ fun getAudioDevices(): AudioRoutesInfo {
|
|
|
1126
868
|
}
|
|
1127
869
|
am.isSpeakerphoneOn = false
|
|
1128
870
|
}
|
|
1129
|
-
currentAudioRoute = "Earpiece"
|
|
1130
|
-
wasManuallySet = false
|
|
1131
871
|
unregisterAudioDeviceCallback()
|
|
1132
872
|
Log.d(TAG, "Audio mode reset to MODE_NORMAL")
|
|
1133
873
|
}
|
|
1134
874
|
}
|
|
1135
875
|
|
|
1136
|
-
//
|
|
1137
|
-
|
|
1138
|
-
|
|
876
|
+
// REVISED: Emit audio route changed event with current route.
|
|
877
|
+
// This is called from MyConnection.onCallEndpointChanged (API 34+)
|
|
878
|
+
// or CallEngine.onTelecomAudioRouteChanged (pre-API 34).
|
|
879
|
+
fun emitAudioRouteChanged(currentRoute: String) {
|
|
880
|
+
val info = getAudioDevices() // Re-query devices for the most up-to-date info
|
|
1139
881
|
val deviceStrings = info.devices.map { it.value }
|
|
1140
882
|
val payload = JSONObject().apply {
|
|
1141
883
|
put("devices", JSONArray(deviceStrings))
|
|
@@ -1157,76 +899,15 @@ fun getAudioDevices(): AudioRoutesInfo {
|
|
|
1157
899
|
}
|
|
1158
900
|
}
|
|
1159
901
|
|
|
902
|
+
// REVISED: This method only triggers an update to JS, it does not
|
|
903
|
+
// influence the actual audio routing as Telecom handles that directly.
|
|
1160
904
|
private fun handleAudioDeviceChange(devices: Array<out AudioDeviceInfo>?, isAdded: Boolean) {
|
|
1161
905
|
if (devices == null || !isCallActive()) return
|
|
1162
906
|
|
|
1163
|
-
|
|
1164
|
-
val currentCallInfo = getCurrentActiveCall()
|
|
1165
|
-
if (currentCallInfo == null) {
|
|
1166
|
-
Log.d(TAG, "No active call, ignoring device change")
|
|
1167
|
-
return
|
|
1168
|
-
}
|
|
907
|
+
Log.d(TAG, "Relevant audio device change detected. Added: $isAdded.")
|
|
1169
908
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
device.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO ||
|
|
1173
|
-
device.type == AudioDeviceInfo.TYPE_BLE_HEADSET ||
|
|
1174
|
-
device.type == AudioDeviceInfo.TYPE_WIRED_HEADSET ||
|
|
1175
|
-
device.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES ||
|
|
1176
|
-
device.type == AudioDeviceInfo.TYPE_USB_HEADSET
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
if (relevantDevices.isEmpty()) {
|
|
1180
|
-
Log.d(TAG, "No relevant devices in change event")
|
|
1181
|
-
return
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
Log.d(TAG, "Relevant device change detected. Added: $isAdded, wasManuallySet: $wasManuallySet")
|
|
1185
|
-
|
|
1186
|
-
if (isAdded && !wasManuallySet) {
|
|
1187
|
-
// Device connected - switch to it automatically if user hasn't manually set route
|
|
1188
|
-
val deviceType = relevantDevices.first().type
|
|
1189
|
-
val newRoute = when (deviceType) {
|
|
1190
|
-
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
|
|
1191
|
-
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
|
|
1192
|
-
AudioDeviceInfo.TYPE_BLE_HEADSET -> "Bluetooth"
|
|
1193
|
-
AudioDeviceInfo.TYPE_WIRED_HEADSET,
|
|
1194
|
-
AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
|
|
1195
|
-
AudioDeviceInfo.TYPE_USB_HEADSET -> "Headset"
|
|
1196
|
-
else -> null
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
if (newRoute != null && newRoute != currentAudioRoute) {
|
|
1200
|
-
Log.d(TAG, "Auto-switching to newly connected device: $newRoute")
|
|
1201
|
-
// Add slight delay to ensure device is ready
|
|
1202
|
-
mainHandler.postDelayed({
|
|
1203
|
-
applyAudioRoute(newRoute)
|
|
1204
|
-
}, 300)
|
|
1205
|
-
}
|
|
1206
|
-
} else if (!isAdded) {
|
|
1207
|
-
// Device disconnected - fall back to appropriate route
|
|
1208
|
-
val disconnectedType = relevantDevices.first().type
|
|
1209
|
-
val wasCurrentRoute = when (disconnectedType) {
|
|
1210
|
-
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
|
|
1211
|
-
AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
|
|
1212
|
-
AudioDeviceInfo.TYPE_BLE_HEADSET -> currentAudioRoute == "Bluetooth"
|
|
1213
|
-
AudioDeviceInfo.TYPE_WIRED_HEADSET,
|
|
1214
|
-
AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
|
|
1215
|
-
AudioDeviceInfo.TYPE_USB_HEADSET -> currentAudioRoute == "Headset"
|
|
1216
|
-
else -> false
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
if (wasCurrentRoute) {
|
|
1220
|
-
Log.d(TAG, "Current audio device disconnected, falling back")
|
|
1221
|
-
// Reset manual flag since the manually selected device is gone
|
|
1222
|
-
wasManuallySet = false
|
|
1223
|
-
mainHandler.postDelayed({
|
|
1224
|
-
setInitialAudioRoute(currentCallInfo.callType)
|
|
1225
|
-
}, 300)
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
// Always emit devices changed event
|
|
909
|
+
// Simply emit that available devices might have changed.
|
|
910
|
+
// Telecom's onCallEndpointChanged (or onCallAudioStateChanged) will report the actual route change.
|
|
1230
911
|
emitAudioDevicesChanged()
|
|
1231
912
|
}
|
|
1232
913
|
|
|
@@ -1234,15 +915,16 @@ fun getAudioDevices(): AudioRoutesInfo {
|
|
|
1234
915
|
return activeCalls.values.find { it.state == CallState.ACTIVE }
|
|
1235
916
|
}
|
|
1236
917
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
918
|
+
// This method gets available devices and emits to JS.
|
|
919
|
+
fun emitAudioDevicesChanged() {
|
|
920
|
+
val info = getAudioDevices()
|
|
921
|
+
val deviceStrings = info.devices.map { it.value }
|
|
922
|
+
val payload = JSONObject().apply {
|
|
923
|
+
put("devices", JSONArray(deviceStrings))
|
|
924
|
+
put("currentRoute", info.currentRoute) // Current route is part of audioDevices info
|
|
925
|
+
}
|
|
926
|
+
emitEvent(CallEventType.AUDIO_DEVICES_CHANGED, payload)
|
|
927
|
+
Log.d(TAG, "Audio devices changed: available: $deviceStrings")
|
|
1246
928
|
}
|
|
1247
929
|
|
|
1248
930
|
fun registerAudioDeviceCallback() {
|
|
@@ -1250,7 +932,7 @@ fun getAudioDevices(): AudioRoutesInfo {
|
|
|
1250
932
|
val context = requireContext()
|
|
1251
933
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
1252
934
|
try {
|
|
1253
|
-
audioManager?.registerAudioDeviceCallback(audioDeviceCallback,
|
|
935
|
+
audioManager?.registerAudioDeviceCallback(audioDeviceCallback, mainHandler) // Pass handler for callback thread
|
|
1254
936
|
Log.d(TAG, "Audio device callback registered")
|
|
1255
937
|
} catch (e: Exception) {
|
|
1256
938
|
Log.w(TAG, "Failed to register audio device callback: ${e.message}")
|
|
@@ -1278,7 +960,8 @@ fun getAudioDevices(): AudioRoutesInfo {
|
|
|
1278
960
|
PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
1279
961
|
"CallEngine:WakeLock"
|
|
1280
962
|
)
|
|
1281
|
-
|
|
963
|
+
// Acquire with a timeout to prevent permanent wakelocks
|
|
964
|
+
wakeLock?.acquire(10 * 60 * 1000L) // 10 minutes
|
|
1282
965
|
Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK")
|
|
1283
966
|
}
|
|
1284
967
|
} else {
|
|
@@ -1313,7 +996,6 @@ fun getAudioDevices(): AudioRoutesInfo {
|
|
|
1313
996
|
put("reason", reason)
|
|
1314
997
|
})
|
|
1315
998
|
|
|
1316
|
-
// Only remove metadata if there's NO existing active call with this ID
|
|
1317
999
|
val existingCall = activeCalls[callId]
|
|
1318
1000
|
if (existingCall == null) {
|
|
1319
1001
|
callMetadata.remove(callId)
|
|
@@ -1398,7 +1080,7 @@ fun getAudioDevices(): AudioRoutesInfo {
|
|
|
1398
1080
|
PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
1399
1081
|
"CallEngine:LockScreenWake"
|
|
1400
1082
|
)
|
|
1401
|
-
wakeLock.acquire(5000)
|
|
1083
|
+
wakeLock.acquire(5000) // Acquire for 5 seconds to ensure activity launch
|
|
1402
1084
|
context.startActivity(overlayIntent)
|
|
1403
1085
|
Log.d(TAG, "Successfully launched CallActivity overlay")
|
|
1404
1086
|
} catch (e: Exception) {
|
|
@@ -1653,7 +1335,10 @@ fun getAudioDevices(): AudioRoutesInfo {
|
|
|
1653
1335
|
|
|
1654
1336
|
private fun startRingback() {
|
|
1655
1337
|
val context = requireContext()
|
|
1656
|
-
if (ringbackPlayer?.isPlaying == true)
|
|
1338
|
+
if (ringbackPlayer?.isPlaying == true) {
|
|
1339
|
+
Log.d(TAG, "Ringback player already playing.")
|
|
1340
|
+
return
|
|
1341
|
+
}
|
|
1657
1342
|
|
|
1658
1343
|
try {
|
|
1659
1344
|
val ringbackUri =
|
|
@@ -1663,6 +1348,7 @@ fun getAudioDevices(): AudioRoutesInfo {
|
|
|
1663
1348
|
isLooping = true
|
|
1664
1349
|
start()
|
|
1665
1350
|
}
|
|
1351
|
+
Log.d(TAG, "Ringback tone started playing")
|
|
1666
1352
|
} catch (e: Exception) {
|
|
1667
1353
|
Log.e(TAG, "Failed to play ringback tone: ${e.message}")
|
|
1668
1354
|
}
|
|
@@ -1672,6 +1358,7 @@ fun getAudioDevices(): AudioRoutesInfo {
|
|
|
1672
1358
|
try {
|
|
1673
1359
|
ringbackPlayer?.stop()
|
|
1674
1360
|
ringbackPlayer?.release()
|
|
1361
|
+
Log.d(TAG, "Ringback tone stopped")
|
|
1675
1362
|
} catch (e: Exception) {
|
|
1676
1363
|
Log.e(TAG, "Error stopping ringback: ${e.message}")
|
|
1677
1364
|
} finally {
|
|
@@ -2,14 +2,19 @@ package com.margelo.nitro.qusaieilouti99.callmanager
|
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.os.Build
|
|
5
|
+
import android.os.Handler
|
|
6
|
+
import android.os.Looper
|
|
7
|
+
import android.os.OutcomeReceiver
|
|
5
8
|
import android.telecom.CallAudioState
|
|
6
9
|
import android.telecom.CallEndpoint
|
|
7
10
|
import android.telecom.Connection
|
|
11
|
+
import android.telecom.Connection.CallEndpointException
|
|
8
12
|
import android.telecom.DisconnectCause
|
|
9
13
|
import android.telecom.VideoProfile
|
|
10
14
|
import android.util.Log
|
|
11
15
|
import org.json.JSONObject
|
|
12
16
|
import java.util.concurrent.Executor
|
|
17
|
+
import java.util.UUID
|
|
13
18
|
|
|
14
19
|
class MyConnection(
|
|
15
20
|
private val context: Context,
|
|
@@ -23,9 +28,11 @@ class MyConnection(
|
|
|
23
28
|
const val TAG = "MyConnection"
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
private var
|
|
28
|
-
private var
|
|
31
|
+
// NEW: Store available endpoints and current endpoint if API >= 34
|
|
32
|
+
private var availableEndpoints: List<CallEndpoint> = emptyList()
|
|
33
|
+
private var currentEndpoint: CallEndpoint? = null
|
|
34
|
+
// Executor for requestCallEndpointChange callbacks (main thread)
|
|
35
|
+
private val mainExecutor: Executor = { runnable -> Handler(Looper.getMainLooper()).post(runnable) }
|
|
29
36
|
|
|
30
37
|
init {
|
|
31
38
|
connectionProperties = Connection.PROPERTY_SELF_MANAGED
|
|
@@ -83,63 +90,57 @@ class MyConnection(
|
|
|
83
90
|
CallEngine.unholdCall(callId)
|
|
84
91
|
}
|
|
85
92
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
// This method is deprecated in API 34. Still used for API < 34.
|
|
94
|
+
@Suppress("DEPRECATION")
|
|
95
|
+
override fun onCallAudioStateChanged(state: CallAudioState) {
|
|
96
|
+
super.onCallAudioStateChanged(state)
|
|
97
|
+
Log.d(TAG, "onCallAudioStateChanged (deprecated): callId=$callId, muted=${state.isMuted}, route=${state.route}")
|
|
90
98
|
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
CallEngine.
|
|
99
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Before API 34
|
|
100
|
+
// For older APIs, CallAudioState is the primary way to get mute & route from Telecom.
|
|
101
|
+
CallEngine.setMutedInternal(callId, state.isMuted) // CallEngine.setMutedInternal handles AudioManager
|
|
102
|
+
CallEngine.onTelecomAudioRouteChanged(callId, state) // Notify CallEngine about the actual route change
|
|
94
103
|
}
|
|
104
|
+
// For API 34+, onMuteStateChanged and onCallEndpointChanged will be used instead.
|
|
95
105
|
}
|
|
96
106
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Log.d(TAG, "Available call endpoints changed for callId: $callId. Count: ${availableEndpoints.size}")
|
|
100
|
-
CallEngine.onAvailableCallEndpointsChanged(callId, availableEndpoints)
|
|
101
|
-
}
|
|
102
|
-
|
|
107
|
+
// NEW (API 34+): Notifies this Connection that its audio mute state has been changed.
|
|
108
|
+
@Suppress("NewApi")
|
|
103
109
|
override fun onMuteStateChanged(isMuted: Boolean) {
|
|
104
110
|
super.onMuteStateChanged(isMuted)
|
|
105
|
-
Log.d(TAG, "
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
lastMuteState = isMuted
|
|
109
|
-
if (isMuted) {
|
|
110
|
-
CallEngine.muteCall(callId)
|
|
111
|
-
} else {
|
|
112
|
-
CallEngine.unmuteCall(callId)
|
|
113
|
-
}
|
|
111
|
+
Log.d(TAG, "onMuteStateChanged: callId=$callId, isMuted=$isMuted")
|
|
112
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // API 34+
|
|
113
|
+
CallEngine.setMutedInternal(callId, isMuted) // Delegate to CallEngine which manages AudioManager
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
//
|
|
118
|
-
@Suppress("
|
|
119
|
-
override fun
|
|
120
|
-
super.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
// NEW (API 34+): Notifies this Connection that the available call endpoints have been changed.
|
|
118
|
+
@Suppress("NewApi")
|
|
119
|
+
override fun onAvailableCallEndpointsChanged(availableEndpoints: List<CallEndpoint>) {
|
|
120
|
+
super.onAvailableCallEndpointsChanged(availableEndpoints)
|
|
121
|
+
Log.d(TAG, "onAvailableCallEndpointsChanged for callId: $callId. Endpoints: ${availableEndpoints.map { "${it.endpointName} (${it.endpointType})" }}")
|
|
122
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // API 34+
|
|
123
|
+
this.availableEndpoints = availableEndpoints
|
|
124
|
+
CallEngine.emitAudioDevicesChanged() // Signal CallEngine to update device list in JS
|
|
125
125
|
}
|
|
126
|
+
}
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
// NEW (API 34+): Notifies this Connection that the audio endpoint has been changed.
|
|
129
|
+
@Suppress("NewApi")
|
|
130
|
+
override fun onCallEndpointChanged(callEndpoint: CallEndpoint) {
|
|
131
|
+
super.onCallEndpointChanged(callEndpoint)
|
|
132
|
+
Log.d(TAG, "onCallEndpointChanged for callId: $callId. New endpoint: ${callEndpoint.endpointName} (type: ${callEndpoint.endpointType})")
|
|
133
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // API 34+
|
|
134
|
+
this.currentEndpoint = callEndpoint
|
|
135
|
+
val routeString = when (callEndpoint.getEndpointType()) {
|
|
136
|
+
CallEndpoint.TYPE_EARPIECE -> "Earpiece"
|
|
137
|
+
CallEndpoint.TYPE_SPEAKER -> "Speaker"
|
|
138
|
+
CallEndpoint.TYPE_BLUETOOTH -> "Bluetooth"
|
|
139
|
+
CallEndpoint.TYPE_WIRED_HEADSET -> "Headset"
|
|
140
|
+
else -> "Unknown"
|
|
134
141
|
}
|
|
142
|
+
CallEngine.emitAudioRouteChanged(routeString) // Notify CallEngine to update JS
|
|
135
143
|
}
|
|
136
|
-
|
|
137
|
-
if (lastAudioState == null || lastAudioState!!.route != state.route) {
|
|
138
|
-
Log.d(TAG, "System audio route changed (legacy) for callId: $callId. Telecom route: ${state.route}")
|
|
139
|
-
CallEngine.onTelecomAudioRouteChanged(callId, state)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
lastAudioState = state
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
override fun onPlayDtmfTone(digit: Char) {
|
|
@@ -163,19 +164,22 @@ class MyConnection(
|
|
|
163
164
|
|
|
164
165
|
override fun onStateChanged(state: Int) {
|
|
165
166
|
super.onStateChanged(state)
|
|
166
|
-
Log.d(TAG, "Connection state changed for callId: $callId. New state: $state")
|
|
167
|
+
Log.d(TAG, "Connection state changed for callId: $callId. New state: ${Connection.stateToString(state)}")
|
|
167
168
|
|
|
168
169
|
when (state) {
|
|
169
170
|
STATE_RINGING -> {
|
|
170
171
|
Log.d(TAG, "Connection is now ringing for callId: $callId")
|
|
172
|
+
CallEngine.setAudioMode() // Ensure audio mode for ringing
|
|
171
173
|
}
|
|
172
174
|
STATE_HOLDING -> {
|
|
173
175
|
Log.d(TAG, "Connection is now holding for callId: $callId")
|
|
174
176
|
}
|
|
175
177
|
STATE_ACTIVE -> {
|
|
176
178
|
Log.d(TAG, "Connection is now active for callId: $callId")
|
|
177
|
-
//
|
|
179
|
+
// Request initial audio route from Telecom when the call becomes active.
|
|
180
|
+
// This ensures Telecom handles the routing change and reports back via callbacks.
|
|
178
181
|
CallEngine.setInitialAudioRouteForCall(callId, callType)
|
|
182
|
+
CallEngine.setAudioMode() // Ensure communication mode is set
|
|
179
183
|
}
|
|
180
184
|
STATE_DISCONNECTED -> {
|
|
181
185
|
Log.d(TAG, "Connection is now disconnected for callId: $callId")
|
|
@@ -184,35 +188,52 @@ class MyConnection(
|
|
|
184
188
|
}
|
|
185
189
|
}
|
|
186
190
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Sets the telecom audio route for this connection. Uses requestCallEndpointChange for API 34+
|
|
193
|
+
* and the deprecated setAudioRoute(int) for older APIs.
|
|
194
|
+
* @param routeInt The desired audio route as a CallAudioState.ROUTE_X constant.
|
|
195
|
+
*/
|
|
196
|
+
fun setTelecomAudioRoute(routeInt: Int) {
|
|
197
|
+
Log.d(TAG, "Attempting to set telecom audio route to int: $routeInt for callId: $callId")
|
|
198
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // API 34+
|
|
199
|
+
// Find the corresponding CallEndpoint from the available list
|
|
200
|
+
val desiredEndpoint = availableEndpoints.find { it.endpointType == routeInt }
|
|
201
|
+
if (desiredEndpoint != null) {
|
|
202
|
+
Log.d(TAG, "Requesting CallEndpoint change to ${desiredEndpoint.endpointName} (type: ${desiredEndpoint.endpointType})")
|
|
203
|
+
try {
|
|
204
|
+
requestCallEndpointChange(desiredEndpoint, mainExecutor, object : OutcomeReceiver<Void, CallEndpointException> {
|
|
205
|
+
override fun onResult(result: Void?) {
|
|
206
|
+
Log.d(TAG, "requestCallEndpointChange succeeded for callId: $callId")
|
|
207
|
+
// No need to manually update state, onCallEndpointChanged will be called by Telecom
|
|
208
|
+
}
|
|
209
|
+
override fun onError(error: CallEndpointException) {
|
|
210
|
+
Log.e(TAG, "requestCallEndpointChange failed for callId: $callId: ${error.message}", error)
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
} catch (e: Exception) {
|
|
214
|
+
Log.e(TAG, "Exception during requestCallEndpointChange for callId: $callId: ${e.message}", e)
|
|
198
215
|
}
|
|
216
|
+
} else {
|
|
217
|
+
Log.w(TAG, "Desired endpoint type $routeInt not found in available endpoints for callId: $callId")
|
|
218
|
+
// Fallback to deprecated for types not found in CallEndpoint list? Or just log and do nothing.
|
|
219
|
+
// For now, log and do nothing as Telecom should provide valid endpoints.
|
|
199
220
|
}
|
|
200
|
-
} else {
|
|
201
|
-
// Fallback to legacy method
|
|
202
|
-
val route = when (endpoint.endpointType) {
|
|
203
|
-
CallEndpoint.TYPE_EARPIECE -> CallAudioState.ROUTE_EARPIECE
|
|
204
|
-
CallEndpoint.TYPE_SPEAKER -> CallAudioState.ROUTE_SPEAKER
|
|
205
|
-
CallEndpoint.TYPE_BLUETOOTH -> CallAudioState.ROUTE_BLUETOOTH
|
|
206
|
-
CallEndpoint.TYPE_WIRED_HEADSET -> CallAudioState.ROUTE_WIRED_HEADSET
|
|
207
|
-
else -> CallAudioState.ROUTE_EARPIECE
|
|
208
|
-
}
|
|
221
|
+
} else { // Fallback for API < 34
|
|
209
222
|
@Suppress("DEPRECATION")
|
|
210
|
-
setAudioRoute(
|
|
223
|
+
setAudioRoute(routeInt)
|
|
224
|
+
Log.d(TAG, "Using deprecated setAudioRoute(int) for callId: $callId")
|
|
211
225
|
}
|
|
212
226
|
}
|
|
213
227
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
228
|
+
// NEW: Expose current CallEndpoint for CallEngine to query (API 34+)
|
|
229
|
+
@Suppress("NewApi")
|
|
230
|
+
fun getCurrentTelecomCallEndpoint(): CallEndpoint? {
|
|
231
|
+
return currentEndpoint
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// NEW: Expose available CallEndpoints for CallEngine to query (API 34+)
|
|
235
|
+
@Suppress("NewApi")
|
|
236
|
+
fun getAvailableTelecomCallEndpoints(): List<CallEndpoint> {
|
|
237
|
+
return availableEndpoints
|
|
217
238
|
}
|
|
218
239
|
}
|
package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/MyConnectionService.kt
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
package com.margelo.nitro.qusaieilouti99.callmanager
|
|
2
2
|
|
|
3
3
|
import android.os.Build
|
|
4
|
+
import android.telecom.CallAudioState
|
|
4
5
|
import android.telecom.Connection
|
|
5
6
|
import android.telecom.ConnectionRequest
|
|
6
7
|
import android.telecom.ConnectionService
|
|
7
8
|
import android.telecom.PhoneAccountHandle
|
|
9
|
+
import android.telecom.TelecomManager
|
|
8
10
|
import android.telecom.VideoProfile
|
|
9
11
|
import android.util.Log
|
|
10
12
|
import java.util.UUID
|
|
@@ -62,18 +64,16 @@ class MyConnectionService : ConnectionService() {
|
|
|
62
64
|
|
|
63
65
|
val videoState = if (isVideoCallBoolean) VideoProfile.STATE_BIDIRECTIONAL else VideoProfile.STATE_AUDIO_ONLY
|
|
64
66
|
connection.setVideoState(videoState)
|
|
67
|
+
connection.setDialing()
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
Log.d(TAG, "
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Log.d(TAG, "Setting outgoing connection as DIALING for callId: $callId")
|
|
73
|
-
connection.setDialing()
|
|
69
|
+
Log.d(TAG, "Created outgoing connection for callId: $callId. Status: DIALING, VideoState: $videoState")
|
|
70
|
+
|
|
71
|
+
if (request.extras?.getBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false) == true) {
|
|
72
|
+
Log.d(TAG, "Hinting Telecom to start outgoing call with speakerphone as per request extras.")
|
|
73
|
+
// Use the new setTelecomAudioRoute on MyConnection to request route change from Telecom
|
|
74
|
+
connection.setTelecomAudioRoute(CallAudioState.ROUTE_SPEAKER)
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
Log.d(TAG, "Created outgoing connection for callId: $callId. VideoState: $videoState")
|
|
77
77
|
return connection
|
|
78
78
|
}
|
|
79
79
|
}
|