@qusaieilouti99/call-manager 0.1.166 → 0.1.167

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.
@@ -34,14 +34,6 @@ import java.util.concurrent.atomic.AtomicBoolean
34
34
  import org.json.JSONArray
35
35
  import org.json.JSONObject
36
36
 
37
- /**
38
- * Core call‐management engine. Manages self-managed telecom calls,
39
- * audio routing, UI notifications, etc.
40
- *
41
- * Audio routing is now fully delegated to the Android Telecom framework,
42
- * which is the correct approach for self-managed calls. This ensures
43
- * consistency and proper handling of device changes (BT, headset, etc.).
44
- */
45
37
  object CallEngine {
46
38
  private const val TAG = "CallEngine"
47
39
  private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
@@ -329,7 +321,6 @@ object CallEngine {
329
321
  val extras = Bundle().apply {
330
322
  putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
331
323
  putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
332
- // Let Telecom decide the initial audio route based on devices and video state
333
324
  putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, isVideoCall)
334
325
  }
335
326
 
@@ -348,11 +339,6 @@ object CallEngine {
348
339
  updateLockScreenBypass()
349
340
  }
350
341
 
351
- /**
352
- * Starts a call and immediately sets it to active.
353
- * Use this for joining an already-established call.
354
- * If a call with the same ID is already incoming, it answers it instead.
355
- */
356
342
  fun startCall(
357
343
  callId: String,
358
344
  callType: String,
@@ -382,8 +368,7 @@ object CallEngine {
382
368
  }
383
369
  }
384
370
 
385
- // This call will be set to ACTIVE immediately by MyConnectionService
386
- activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
371
+ activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.DIALING)
387
372
  currentCallId = callId
388
373
  Log.d(TAG, "Call $callId will be started as ACTIVE.")
389
374
 
@@ -399,7 +384,7 @@ object CallEngine {
399
384
  putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
400
385
  putString(MyConnectionService.EXTRA_DISPLAY_NAME, targetName)
401
386
  putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, callType == "Video")
402
- putBoolean(MyConnectionService.EXTRA_START_IMMEDIATELY_ACTIVE, true) // Custom flag
387
+ putBoolean(MyConnectionService.EXTRA_START_IMMEDIATELY_ACTIVE, true)
403
388
  metadata?.let { putString("metadata", it) }
404
389
  }
405
390
 
@@ -414,7 +399,6 @@ object CallEngine {
414
399
  bringAppToForeground()
415
400
  keepScreenAwake(true)
416
401
  updateLockScreenBypass()
417
- // Event is emitted by coreCallAnswered once the connection is active
418
402
  Log.d(TAG, "Successfully placed call to be immediately active with TelecomManager.")
419
403
  } catch (e: Exception) {
420
404
  Log.e(TAG, "Failed to start call: ${e.message}", e)
@@ -429,7 +413,12 @@ object CallEngine {
429
413
 
430
414
  fun answerCall(callId: String) {
431
415
  Log.d(TAG, "answerCall: $callId - local party answering")
432
- coreCallAnswered(callId, isLocalAnswer = true)
416
+ val connection = telecomConnections[callId]
417
+ if (connection != null) {
418
+ connection.setActive()
419
+ } else {
420
+ Log.e(TAG, "Cannot answer call, no connection found for $callId")
421
+ }
433
422
  }
434
423
 
435
424
  fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
@@ -440,8 +429,6 @@ object CallEngine {
440
429
  return
441
430
  }
442
431
 
443
- // The connection state change (e.g., onAnswer) is the source of truth.
444
- // We just update our internal state and UI.
445
432
  activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
446
433
  currentCallId = callId
447
434
  Log.d(TAG, "Call $callId set to ACTIVE state")
@@ -463,9 +450,6 @@ object CallEngine {
463
450
  keepScreenAwake(true)
464
451
  updateLockScreenBypass()
465
452
 
466
- // Audio is now managed by the Connection. We don't need to do anything here.
467
- // The initial audio route is set by MyConnection when it becomes active.
468
-
469
453
  if (isLocalAnswer) {
470
454
  emitCallAnsweredWithMetadata(callId)
471
455
  } else {
@@ -587,13 +571,12 @@ object CallEngine {
587
571
  }
588
572
 
589
573
  private fun setMutedInternal(callId: String, muted: Boolean) {
590
- val callInfo = activeCalls[callId]
591
- if (callInfo == null) {
574
+ val connection = telecomConnections[callId] as? MyConnection ?: run {
592
575
  Log.w(TAG, "Cannot set mute state for call $callId - not found")
593
576
  return
594
577
  }
595
- // The Connection's onCallAudioStateChanged will handle the event emission
596
- (telecomConnections[callId] as? MyConnection)?.setMuted(muted)
578
+ // Delegate the action to the connection, which will inform Telecom
579
+ connection.setMuted(muted)
597
580
  }
598
581
 
599
582
  fun endCall(callId: String) {
@@ -675,7 +658,7 @@ object CallEngine {
675
658
  })
676
659
  }
677
660
 
678
- // ====== NEW TELECOM-DRIVEN AUDIO ROUTING SYSTEM ======
661
+ // ====== TELECOM-DRIVEN AUDIO ROUTING SYSTEM ======
679
662
 
680
663
  fun getAudioDevices(): AudioRoutesInfo {
681
664
  val context = requireContext()
@@ -726,35 +709,11 @@ object CallEngine {
726
709
  connection.requestAudioRouteChange(telecomRoute)
727
710
  }
728
711
 
729
- /**
730
- * Called by MyConnection when the audio route changes. This is the single
731
- * source of truth for route updates.
732
- */
733
712
  fun onTelecomAudioRouteChanged(callId: String, newRoute: String) {
734
713
  Log.d(TAG, "onTelecomAudioRouteChanged for callId $callId, new route: $newRoute")
735
714
  emitAudioRouteChanged()
736
715
  }
737
716
 
738
- /**
739
- * Called by MyConnection when it becomes active to set the initial, logical
740
- * audio route.
741
- */
742
- fun setInitialAudioRouteForCall(callId: String, callType: String) {
743
- val am = audioManager ?: return
744
- val connection = telecomConnections[callId] as? MyConnection ?: return
745
-
746
- // Determine default route based on Android standards
747
- val defaultRoute = when {
748
- connection.isBluetoothAvailable() -> "Bluetooth"
749
- am.isWiredHeadsetOn -> "Headset"
750
- callType.equals("Video", ignoreCase = true) -> "Speaker"
751
- else -> "Earpiece"
752
- }
753
-
754
- Log.d(TAG, "Requesting initial audio route for call $callId: $defaultRoute")
755
- setAudioRoute(defaultRoute)
756
- }
757
-
758
717
  private fun emitAudioRouteChanged() {
759
718
  val info = getAudioDevices()
760
719
  val deviceStrings = info.devices.map { it.value }
@@ -813,12 +772,11 @@ object CallEngine {
813
772
  })
814
773
 
815
774
  // Only remove metadata if there's NO existing active call with this ID
816
- val existingCall = activeCalls[callId]
817
- if (existingCall == null) {
818
- callMetadata.remove(callId)
819
- Log.d(TAG, "Removed metadata for rejected call $callId (no existing call)")
775
+ if (!activeCalls.containsKey(callId)) {
776
+ callMetadata.remove(callId)
777
+ Log.d(TAG, "Removed metadata for rejected call $callId (no existing call)")
820
778
  } else {
821
- Log.d(TAG, "Kept metadata for callId: $callId (existing call: ${existingCall.state})")
779
+ Log.d(TAG, "Kept metadata for callId: $callId (an existing call is using it)")
822
780
  }
823
781
  }
824
782
 
@@ -836,7 +794,6 @@ object CallEngine {
836
794
  enableVibration(true)
837
795
  setBypassDnd(true)
838
796
  lockscreenVisibility = Notification.VISIBILITY_PUBLIC
839
- // Sound is handled by the RingtoneManager directly for better control
840
797
  setSound(null, null)
841
798
  }
842
799
  val manager = context.getSystemService(NotificationManager::class.java)
@@ -1141,7 +1098,6 @@ object CallEngine {
1141
1098
  Log.d(TAG, "Performing cleanup")
1142
1099
  stopForegroundService()
1143
1100
  keepScreenAwake(false)
1144
- // Reset audio mode via AudioManager when all calls are truly gone
1145
1101
  audioManager?.mode = AudioManager.MODE_NORMAL
1146
1102
  }
1147
1103
 
@@ -4,6 +4,7 @@ import android.os.Build
4
4
  import android.os.OutcomeReceiver
5
5
  import android.telecom.CallAudioState
6
6
  import android.telecom.CallEndpoint
7
+ import android.telecom.CallEndpointException
7
8
  import android.telecom.Connection
8
9
  import android.telecom.DisconnectCause
9
10
  import android.telecom.VideoProfile
@@ -22,13 +23,11 @@ class MyConnection(
22
23
  const val TAG = "MyConnection"
23
24
  }
24
25
 
25
- private var lastAudioState: CallAudioState? = null
26
26
  private val telecomExecutor = Executors.newSingleThreadExecutor()
27
27
 
28
28
  init {
29
29
  connectionProperties = PROPERTY_SELF_MANAGED
30
30
  connectionCapabilities = CAPABILITY_SUPPORT_HOLD or CAPABILITY_MUTE or CAPABILITY_HOLD
31
-
32
31
  audioModeIsVoip = true
33
32
 
34
33
  if (callType == "Video") {
@@ -43,6 +42,8 @@ class MyConnection(
43
42
  Log.d(TAG, "MyConnection for callId $callId created and added to CallEngine.")
44
43
  }
45
44
 
45
+ // --- Lifecycle Callbacks ---
46
+
46
47
  override fun onSilence() {
47
48
  super.onSilence()
48
49
  Log.d(TAG, "onSilence called by system for callId: $callId. Silencing ringtone.")
@@ -51,7 +52,7 @@ class MyConnection(
51
52
 
52
53
  override fun onAnswer() {
53
54
  Log.d(TAG, "Call answered via Telecom for callId: $callId")
54
- setActive()
55
+ // State will be set to ACTIVE, which triggers onStateChanged
55
56
  CallEngine.answerCall(callId)
56
57
  }
57
58
 
@@ -81,25 +82,7 @@ class MyConnection(
81
82
  CallEngine.unholdCall(callId)
82
83
  }
83
84
 
84
- override fun onCallAudioStateChanged(state: CallAudioState) {
85
- super.onCallAudioStateChanged(state)
86
- Log.d(TAG, "onCallAudioStateChanged for callId: $callId. muted=${state.isMuted}, route=${state.routeToString()}")
87
-
88
- // Mute state change
89
- if (lastAudioState?.isMuted != state.isMuted) {
90
- val eventType = if (state.isMuted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
91
- CallEngine.emitEvent(eventType, JSONObject().put("callId", callId))
92
- Log.d(TAG, "Call $callId mute state changed to: ${state.isMuted}")
93
- }
94
-
95
- // Route change
96
- if (lastAudioState?.route != state.route) {
97
- Log.d(TAG, "System audio route changed for callId: $callId. New route: ${state.routeToString()}")
98
- CallEngine.onTelecomAudioRouteChanged(callId, state.routeToString())
99
- }
100
-
101
- lastAudioState = state
102
- }
85
+ // --- State Change Callbacks ---
103
86
 
104
87
  override fun onStateChanged(state: Int) {
105
88
  super.onStateChanged(state)
@@ -107,12 +90,13 @@ class MyConnection(
107
90
 
108
91
  when (state) {
109
92
  STATE_ACTIVE -> {
110
- // When the call becomes active, set the initial audio route.
111
- // This is the correct place to do it.
112
- Log.d(TAG, "Connection is now active for callId: $callId. Setting initial audio route.")
113
- CallEngine.setInitialAudioRouteForCall(callId, callType)
93
+ Log.d(TAG, "Connection is now active for callId: $callId.")
114
94
  // If this was an immediate-start call, trigger the answered event.
115
- CallEngine.coreCallAnswered(callId, isLocalAnswer = false)
95
+ // Otherwise, this is handled by onAnswer -> answerCall.
96
+ val callInfo = CallEngine.getActiveCalls().find { it.callId == callId }
97
+ if (callInfo?.state != CallState.ACTIVE) {
98
+ CallEngine.coreCallAnswered(callId, isLocalAnswer = true)
99
+ }
116
100
  }
117
101
  STATE_DISCONNECTED -> {
118
102
  Log.d(TAG, "Connection is now disconnected for callId: $callId")
@@ -121,17 +105,37 @@ class MyConnection(
121
105
  }
122
106
  }
123
107
 
108
+ override fun onCallAudioStateChanged(state: CallAudioState) {
109
+ super.onCallAudioStateChanged(state)
110
+ Log.d(TAG, "onCallAudioStateChanged for callId: $callId. muted=${state.isMuted}, route=${routeToString(state.route)}")
111
+
112
+ // This callback is the source of truth from the system.
113
+ // We just need to forward these state changes to our engine/UI.
114
+ CallEngine.onTelecomAudioRouteChanged(callId, routeToString(state.route))
115
+
116
+ // Emit mute/unmute events based on state changes from the system
117
+ val eventType = if (state.isMuted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
118
+ CallEngine.emitEvent(eventType, JSONObject().put("callId", callId))
119
+ }
120
+
121
+ // --- Audio Control Methods (Called from CallEngine) ---
122
+
123
+ /**
124
+ * Requests a change in the audio route (Speaker, Earpiece, etc.).
125
+ * This is the ONLY method that should be used to change the audio route.
126
+ */
124
127
  fun requestAudioRouteChange(route: Int) {
125
128
  Log.d(TAG, "Requesting audio route change to: ${routeToString(route)} for callId: $callId")
126
129
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
127
- val endpoint = availableCallEndpoints.find { it.endpointType == route }
130
+ // For API 34+, use the modern CallEndpoint API
131
+ val endpoint = this.callAudioState?.availableCallEndpoints?.find { it.type == route }
128
132
  if (endpoint != null) {
129
133
  Log.d(TAG, "Using requestCallEndpointChange for API 34+")
130
- requestCallEndpointChange(endpoint, telecomExecutor, object : OutcomeReceiver<Void, Exception> {
134
+ requestCallEndpointChange(endpoint, telecomExecutor, object : OutcomeReceiver<Void, CallEndpointException> {
131
135
  override fun onResult(result: Void?) {
132
- Log.d(TAG, "Successfully changed audio endpoint to ${endpoint.endpointName}")
136
+ Log.d(TAG, "Successfully requested audio endpoint change to ${endpoint.name}")
133
137
  }
134
- override fun onError(error: Exception) {
138
+ override fun onError(error: CallEndpointException) {
135
139
  Log.e(TAG, "Failed to change audio endpoint", error)
136
140
  }
137
141
  })
@@ -139,23 +143,37 @@ class MyConnection(
139
143
  Log.w(TAG, "Could not find a matching CallEndpoint for route: ${routeToString(route)}")
140
144
  }
141
145
  } else {
146
+ // For older APIs, use the deprecated setAudioRoute
142
147
  Log.d(TAG, "Using setAudioRoute (deprecated) for older API")
143
148
  @Suppress("DEPRECATION")
144
149
  setAudioRoute(route)
145
150
  }
146
151
  }
147
152
 
148
- fun isBluetoothAvailable(): Boolean {
149
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
150
- this.callAudioState?.supportedRouteMask?.and(CallAudioState.ROUTE_BLUETOETOOTH) != 0
151
- } else {
152
- // Fallback for older versions
153
- this.callAudioState?.supportedRouteMask?.and(CallAudioState.ROUTE_BLUETOOTH) != 0
154
- }
153
+ /**
154
+ * Informs the Telecom framework about a mute state change initiated by our app.
155
+ */
156
+ fun setMuted(muted: Boolean) {
157
+ val currentState = this.callAudioState ?: return
158
+ if (currentState.isMuted == muted) return // No change needed
159
+
160
+ val newState = CallAudioState(muted, currentState.route, currentState.supportedRouteMask)
161
+ // This call informs the system of the change. The system will then call
162
+ // onCallAudioStateChanged back on this connection instance with the new state.
163
+ onCallAudioStateChanged(newState)
164
+ Log.d(TAG, "Informed Telecom of mute state change to: $muted")
155
165
  }
156
166
 
167
+
168
+ // --- Helper Functions ---
169
+
157
170
  fun getCurrentRouteString(): String {
158
- return lastAudioState?.route.let { routeToString(it) }
171
+ return this.callAudioState?.route.let { routeToString(it) }
172
+ }
173
+
174
+ fun isBluetoothAvailable(): Boolean {
175
+ val mask = this.callAudioState?.supportedRouteMask ?: 0
176
+ return mask and CallAudioState.ROUTE_BLUETOOTH != 0
159
177
  }
160
178
 
161
179
  private fun routeToString(route: Int?): String {
@@ -167,4 +185,18 @@ class MyConnection(
167
185
  else -> "Unknown"
168
186
  }
169
187
  }
188
+
189
+ private fun stateToString(state: Int): String {
190
+ return when (state) {
191
+ STATE_INITIALIZING -> "INITIALIZING"
192
+ STATE_NEW -> "NEW"
193
+ STATE_RINGING -> "RINGING"
194
+ STATE_DIALING -> "DIALING"
195
+ STATE_ACTIVE -> "ACTIVE"
196
+ STATE_HOLDING -> "HOLDING"
197
+ STATE_DISCONNECTED -> "DISCONNECTED"
198
+ STATE_PULLING_CALL -> "PULLING_CALL"
199
+ else -> "UNKNOWN"
200
+ }
201
+ }
170
202
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.166",
3
+ "version": "0.1.167",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",