@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.
@@ -1,6 +1,7 @@
1
1
  package com.margelo.nitro.qusaieilouti99.callmanager
2
- import android.telecom.CallAudioState
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
- private var currentAudioRoute: String = "Earpiece"
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
- callId: String,
367
- callType: String,
368
- targetName: String,
369
- metadata: String? = null
367
+ callId: String,
368
+ callType: String,
369
+ targetName: String,
370
+ metadata: String? = null
370
371
  ) {
371
- Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
372
- metadata?.let { callMetadata[callId] = it }
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
- // Register audio device callback to handle dynamic device changes
407
- registerAudioDeviceCallback()
375
+ if (activeCalls.containsKey(callId)) {
376
+ Log.w(TAG, "Call $callId already exists, cannot start again")
377
+ return
378
+ }
408
379
 
409
- // Set initial audio route with proper timing
410
- mainHandler.postDelayed({
411
- setInitialAudioRouteForCall(callId, callType)
412
- }, 500L)
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
- updateLockScreenBypass()
415
- emitOutgoingCallAnsweredWithMetadata(callId)
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
- // Report to telecom as an outgoing call that immediately becomes active
418
- reportOutgoingCallToTelecom(callId, callType, targetName, metadata)
419
- } else {
420
- Log.w(TAG, "startCall called for existing call in state: ${existingCall.state}")
421
- }
422
- }
394
+ registerPhoneAccount()
395
+ setAudioMode() // Set audio mode early
396
+ bringAppToForeground()
397
+ startForegroundService()
398
+ keepScreenAwake(true)
423
399
 
424
- private fun reportOutgoingCallToTelecom(
425
- callId: String,
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
- val extras = Bundle().apply {
445
- putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
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
- try {
451
- telecomManager.placeCall(addressUri, extras)
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
- // Set initial audio route with proper timing
505
- mainHandler.postDelayed({
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
- private fun setMutedInternal(callId: String, muted: Boolean) {
630
- val callInfo = activeCalls[callId]
631
- if (callInfo == null) {
632
- Log.w(TAG, "Cannot set mute state for call $callId - not found")
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
- ?: return AudioRoutesInfo(emptyArray(), "Unknown")
688
+ ?: return AudioRoutesInfo(emptyArray(), "Unknown")
743
689
 
744
690
  val devices = mutableSetOf<String>()
745
- var hasWiredHeadset = false
746
- var hasBluetoothDevice = false
747
-
748
- // Always available
749
- devices.add("Earpiece")
750
- devices.add("Speaker")
751
-
752
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
753
- val outputDevices = audioManager?.getDevices(AudioManager.GET_DEVICES_OUTPUTS) ?: emptyArray()
754
- for (device in outputDevices) {
755
- when (device.type) {
756
- AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
757
- AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> {
758
- if (!hasBluetoothDevice) {
759
- devices.add("Bluetooth")
760
- hasBluetoothDevice = true
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
- val deviceHolders = devices.map { StringHolder(it) }.toTypedArray()
797
- return AudioRoutesInfo(deviceHolders, current)
798
- }
799
-
800
- // NEW: Handle modern telecom call endpoint changes (API 34+)
801
- fun onTelecomCallEndpointChanged(callId: String, callEndpoint: CallEndpoint) {
802
- Log.d(TAG, "Telecom call endpoint changed for $callId: ${callEndpoint.endpointName}, type=${callEndpoint.endpointType}")
803
-
804
- val routeString = when (callEndpoint.endpointType) {
805
- CallEndpoint.TYPE_EARPIECE -> "Earpiece"
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
- currentAudioRoute = routeString
813
- Log.d(TAG, "Emitting AUDIO_ROUTE_CHANGED: currentRoute=$routeString")
814
- emitAudioRouteChanged(routeString)
815
- }
816
-
817
- // NEW: Handle available call endpoints change (API 34+)
818
- fun onAvailableCallEndpointsChanged(callId: String, availableEndpoints: List<CallEndpoint>) {
819
- Log.d(TAG, "Available call endpoints changed for $callId: ${availableEndpoints.size} endpoints")
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
- val availableRoutes = availableEndpoints.map { endpoint ->
822
- when (endpoint.endpointType) {
823
- CallEndpoint.TYPE_EARPIECE -> "Earpiece"
824
- CallEndpoint.TYPE_SPEAKER -> "Speaker"
825
- CallEndpoint.TYPE_BLUETOOTH -> "Bluetooth"
826
- CallEndpoint.TYPE_WIRED_HEADSET -> "Headset"
827
- else -> "Unknown"
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
- Log.d(TAG, "Available routes: $availableRoutes")
832
- emitAudioDevicesChanged()
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
- // Handle legacy audio route changes (API 28-33)
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
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
838
- return // Use modern system instead
839
- }
840
-
841
- Log.d(TAG, "Telecom audio route changed (legacy) for $callId: route=${audioState.route}")
842
-
843
- val routeString = when (audioState.route) {
844
- CallAudioState.ROUTE_EARPIECE -> "Earpiece"
845
- CallAudioState.ROUTE_SPEAKER -> "Speaker"
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
- // Set initial audio route using appropriate API
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
- // Don't override if user manually set a route
860
- if (wasManuallySet) {
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
- mainHandler.postDelayed({
866
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
867
- setInitialAudioRouteModern(callId, callType)
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
- telecomConnections[callId]?.let { connection ->
915
- if (connection is MyConnection) {
916
- @Suppress("DEPRECATION")
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
- // UPDATED: Use appropriate API for manual route changes
941
- fun setAudioRoute(route: String) {
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
- wasManuallySet = true
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
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
949
- val endpointType = when (route) {
950
- "Speaker" -> CallEndpoint.TYPE_SPEAKER
951
- "Earpiece" -> CallEndpoint.TYPE_EARPIECE
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 applyAudioRoute(route: String) {
982
- val ctx = requireContext()
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
- // Only emit event if route actually changed
1040
- if (currentAudioRoute != previousRoute) {
1041
- emitAudioRouteChanged(currentAudioRoute)
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 getCurrentAudioRoute(): String {
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 isBluetoothDeviceConnected(): Boolean {
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
- // UPDATED: Fix the method signature
1137
- private fun emitAudioRouteChanged(currentRoute: String) {
1138
- val info = getAudioDevices()
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
- val context = requireContext()
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
- val relevantDevices = devices.filter { device ->
1171
- device.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
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
- private fun emitAudioDevicesChanged() {
1238
- val info = getAudioDevices()
1239
- val deviceStrings = info.devices.map { it.value }
1240
- val payload = JSONObject().apply {
1241
- put("devices", JSONArray(deviceStrings))
1242
- put("currentRoute", info.currentRoute)
1243
- }
1244
- emitEvent(CallEventType.AUDIO_DEVICES_CHANGED, payload)
1245
- Log.d(TAG, "Audio devices changed: available: $deviceStrings")
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, null)
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
- wakeLock?.acquire(10 * 60 * 1000L)
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) return
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
- private var lastAudioState: CallAudioState? = null
27
- private var lastCallEndpoint: CallEndpoint? = null
28
- private var lastMuteState: Boolean? = null
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
- // ====== MODERN AUDIO ROUTING (API 34+) ======
87
- override fun onCallEndpointChanged(callEndpoint: CallEndpoint) {
88
- super.onCallEndpointChanged(callEndpoint)
89
- Log.d(TAG, "Call endpoint changed for callId: $callId. New endpoint: ${callEndpoint.endpointName}, type: ${callEndpoint.endpointType}")
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 (lastCallEndpoint == null || !endpointsEqual(lastCallEndpoint!!, callEndpoint)) {
92
- lastCallEndpoint = callEndpoint
93
- CallEngine.onTelecomCallEndpointChanged(callId, callEndpoint)
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
- override fun onAvailableCallEndpointsChanged(availableEndpoints: List<CallEndpoint>) {
98
- super.onAvailableCallEndpointsChanged(availableEndpoints)
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, "Mute state changed for callId: $callId. Muted: $isMuted")
106
-
107
- if (lastMuteState == null || lastMuteState != isMuted) {
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
- // ====== LEGACY AUDIO ROUTING (API 28-33) ======
118
- @Suppress("DEPRECATION")
119
- override fun onCallAudioStateChanged(state: CallAudioState) {
120
- super.onCallAudioStateChanged(state)
121
-
122
- // Only use legacy system if we're on API < 34
123
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
124
- return // Use modern system instead
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
- Log.d(TAG, "Audio state changed (legacy) for callId: $callId. muted=${state.isMuted}, route=${state.route}")
128
-
129
- if (lastAudioState == null || lastAudioState!!.isMuted != state.isMuted) {
130
- if (state.isMuted) {
131
- CallEngine.muteCall(callId)
132
- } else {
133
- CallEngine.unmuteCall(callId)
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
- // Set initial audio route when call becomes active
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
- // ====== MODERN AUDIO ROUTE CHANGE REQUEST (API 34+) ======
188
- fun requestTelecomAudioRouteChange(endpoint: CallEndpoint) {
189
- Log.d(TAG, "Requesting telecom audio route change to: ${endpoint.endpointName} for callId: $callId")
190
-
191
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
192
- val executor = Executor { command -> command.run() }
193
- requestCallEndpointChange(endpoint, executor) { result ->
194
- result.onSuccess {
195
- Log.d(TAG, "Audio route change successful for callId: $callId")
196
- }.onFailure { exception ->
197
- Log.e(TAG, "Audio route change failed for callId: $callId", exception)
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(route)
223
+ setAudioRoute(routeInt)
224
+ Log.d(TAG, "Using deprecated setAudioRoute(int) for callId: $callId")
211
225
  }
212
226
  }
213
227
 
214
- private fun endpointsEqual(endpoint1: CallEndpoint, endpoint2: CallEndpoint): Boolean {
215
- return endpoint1.endpointType == endpoint2.endpointType &&
216
- endpoint1.identifier == endpoint2.identifier
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
  }
@@ -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
- // Check if this is a startCall scenario (should immediately go to ACTIVE)
67
- val existingCallInfo = CallEngine.getActiveCalls().find { it.callId == callId }
68
- if (existingCallInfo?.state == CallState.ACTIVE) {
69
- Log.d(TAG, "Setting outgoing connection as ACTIVE immediately for callId: $callId")
70
- connection.setActive()
71
- } else {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.170",
3
+ "version": "0.1.172",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",