@qusaieilouti99/call-manager 0.1.37 → 0.1.39

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.
@@ -12,16 +12,16 @@ import android.media.MediaPlayer
12
12
  import android.media.RingtoneManager
13
13
  import android.net.Uri
14
14
  import android.os.Build
15
- import android.os.Bundle // Explicit import for Bundle
15
+ import android.os.Bundle
16
16
  import android.os.PowerManager
17
- import android.telecom.CallAudioState // Explicit import
18
- import android.telecom.Connection // Explicit import
19
- import android.telecom.DisconnectCause // Explicit import
20
- import android.telecom.PhoneAccount // Explicit import
21
- import android.telecom.PhoneAccountHandle // Explicit import
22
- import android.telecom.TelecomManager // Explicit import
23
- import android.telecom.VideoProfile // Explicit import
24
- import android.util.Log // Explicit import for Log
17
+ import android.telecom.CallAudioState
18
+ import android.telecom.Connection
19
+ import android.telecom.DisconnectCause
20
+ import android.telecom.PhoneAccount
21
+ import android.telecom.PhoneAccountHandle
22
+ import android.telecom.TelecomManager
23
+ import android.telecom.VideoProfile
24
+ import android.util.Log
25
25
  import android.graphics.Color
26
26
  import android.app.Person
27
27
  import org.json.JSONArray
@@ -38,22 +38,22 @@ object CallEngine {
38
38
  private const val FOREGROUND_NOTIF_ID = 1001
39
39
 
40
40
  private var ringtone: android.media.Ringtone? = null
41
- private var ringbackPlayer: MediaPlayer? = null // For outgoing call ringback
41
+ private var ringbackPlayer: MediaPlayer? = null
42
42
  private var audioManager: AudioManager? = null
43
43
  private var wakeLock: PowerManager.WakeLock? = null
44
44
  private var appContext: Context? = null
45
45
 
46
46
  // --- Multi-call state ---
47
- private val activeCalls = ConcurrentHashMap<String, CallInfo>() // callId -> CallInfo
48
- private val telecomConnections = ConcurrentHashMap<String, Connection>() // callId -> Telecom Connection
49
- private var currentCallId: String? = null // The call currently in foreground (active/ringing/dialing)
47
+ private val activeCalls = ConcurrentHashMap<String, CallInfo>()
48
+ private val telecomConnections = ConcurrentHashMap<String, Connection>()
49
+ private var currentCallId: String? = null
50
50
  private var canMakeMultipleCalls: Boolean = true
51
51
 
52
52
  data class CallInfo(
53
53
  val callId: String,
54
54
  val callData: String,
55
55
  var state: CallState,
56
- val callType: String = "Audio" // "Audio" or "Video"
56
+ val callType: String = "Audio"
57
57
  )
58
58
 
59
59
  enum class CallState {
@@ -62,12 +62,11 @@ object CallEngine {
62
62
 
63
63
  // --- Event handler for JS ---
64
64
  private var eventHandler: ((CallEventType, String) -> Unit)? = null
65
- private val cachedEvents = mutableListOf<Pair<CallEventType, String>>() // For caching events
65
+ private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
66
66
 
67
67
  fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
68
68
  Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
69
69
  eventHandler = handler
70
- // Emit cached events if a handler is now available
71
70
  handler?.let { h ->
72
71
  if (cachedEvents.isNotEmpty()) {
73
72
  Log.d(TAG, "Emitting ${cachedEvents.size} cached events.")
@@ -133,12 +132,12 @@ object CallEngine {
133
132
  json.optString("name", "Unknown")
134
133
  } catch (e: Exception) { "Unknown" }
135
134
 
136
- val parsedCallType = try { // Extract callType from callData
135
+ val parsedCallType = try {
137
136
  val json = JSONObject(callData)
138
137
  json.optString("callType", "Audio")
139
138
  } catch (e: Exception) { "Audio" }
140
139
 
141
- val isVideoCallBoolean = parsedCallType == "Video" // Convert to boolean for Telecom API
140
+ val isVideoCallBoolean = parsedCallType == "Video"
142
141
 
143
142
  if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
144
143
  Log.d(TAG, "Can't make multiple calls, holding existing calls.")
@@ -154,8 +153,8 @@ object CallEngine {
154
153
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
155
154
  val phoneAccountHandle = getPhoneAccountHandle(context)
156
155
  val extras = Bundle().apply {
157
- putString(MyConnectionService.EXTRA_CALL_DATA, callData) // Pass raw callData
158
- putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, isVideoCallBoolean) // Pass boolean for Telecom
156
+ putString(MyConnectionService.EXTRA_CALL_DATA, callData)
157
+ putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, isVideoCallBoolean)
159
158
  }
160
159
  try {
161
160
  telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
@@ -171,6 +170,7 @@ object CallEngine {
171
170
  notifyCallStateChanged(context)
172
171
  }
173
172
 
173
+ // CORRECTED: Uses TelecomManager.placeCall for outgoing calls.
174
174
  fun startOutgoingCall(context: Context, callId: String, callData: String) {
175
175
  appContext = context.applicationContext
176
176
  Log.d(TAG, "startOutgoingCall: $callId, $callData")
@@ -180,12 +180,12 @@ object CallEngine {
180
180
  json.optString("name", "Unknown")
181
181
  } catch (e: Exception) { "Unknown" }
182
182
 
183
- val parsedCallType = try { // Extract callType from callData
183
+ val parsedCallType = try {
184
184
  val json = JSONObject(callData)
185
185
  json.optString("callType", "Audio")
186
186
  } catch (e: Exception) { "Audio" }
187
187
 
188
- val isVideoCallBoolean = parsedCallType == "Video" // Convert to boolean for Telecom API
188
+ val isVideoCallBoolean = parsedCallType == "Video"
189
189
 
190
190
  if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
191
191
  Log.d(TAG, "Can't make multiple calls, holding existing calls before outgoing.")
@@ -199,33 +199,33 @@ object CallEngine {
199
199
  registerPhoneAccount(context)
200
200
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
201
201
  val phoneAccountHandle = getPhoneAccountHandle(context)
202
+
203
+ val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, callId, null)
204
+
202
205
  val extras = Bundle().apply {
203
- putString(MyConnectionService.EXTRA_CALL_DATA, callData) // Pass raw callData
204
- putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, isVideoCallBoolean) // Pass boolean for Telecom
205
- // Set speaker for video calls initially in Telecom's extras
206
+ putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
207
+ putString(MyConnectionService.EXTRA_CALL_DATA, callData)
208
+ putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, isVideoCallBoolean)
206
209
  putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, isVideoCallBoolean)
207
210
  }
208
211
 
209
212
  try {
210
- // Error: Unresolved reference 'addNewOutgoingCall'. This usually means TelecomManager is not correctly imported
211
- // or the minSdkVersion is too low for this method (requires API 23+).
212
- // Imports already checked; assuming minSdkVersion 26+.
213
- telecomManager.addNewOutgoingCall(phoneAccountHandle, extras)
213
+ telecomManager.placeCall(addressUri, extras)
214
214
  startForegroundService(context)
215
- Log.d(TAG, "Successfully reported outgoing call to TelecomManager for $callId")
215
+ Log.d(TAG, "Successfully reported outgoing call to TelecomManager via placeCall for $callId")
216
216
  startRingback()
217
217
  bringAppToForeground(context)
218
218
  keepScreenAwake(context, true)
219
- if (parsedCallType == "Video") { // Initial audio route for video calls
219
+ if (parsedCallType == "Video") {
220
220
  setAudioRoute(context, "Speaker")
221
221
  } else {
222
- setAudioRoute(context, "Earpiece") // Initial audio route for audio calls
222
+ setAudioRoute(context, "Earpiece")
223
223
  }
224
224
  } catch (e: SecurityException) {
225
- Log.e(TAG, "SecurityException: Failed to start outgoing call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
225
+ Log.e(TAG, "SecurityException: Failed to start outgoing call via placeCall. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
226
226
  endCall(context, callId)
227
227
  } catch (e: Exception) {
228
- Log.e(TAG, "Failed to start outgoing call: ${e.message}", e)
228
+ Log.e(TAG, "Failed to start outgoing call via placeCall: ${e.message}", e)
229
229
  endCall(context, callId)
230
230
  }
231
231
  notifyCallStateChanged(context)
@@ -262,7 +262,6 @@ object CallEngine {
262
262
 
263
263
  fun holdCall(context: Context, callId: String) {
264
264
  Log.d(TAG, "holdCall: $callId")
265
- activeCalls[callId]?.state = CallState.HELD
266
265
  val connection = telecomConnections[callId]
267
266
  connection?.setOnHold()
268
267
  emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
@@ -316,7 +315,7 @@ object CallEngine {
316
315
  removeTelecomConnection(callId)
317
316
  Log.d(TAG, "Telecom Connection for $callId disconnected and destroyed.")
318
317
  } else {
319
- Log.w(TAG, "No Telecom Connection found for callId=$callId. It might have already been handled by Telecom.")
318
+ Log.d(TAG, "No Telecom Connection found for callId=$callId. Likely an outgoing call not initially handled by Telecom.")
320
319
  }
321
320
 
322
321
  if (activeCalls.isEmpty()) {
@@ -445,11 +444,10 @@ object CallEngine {
445
444
 
446
445
  // --- Audio Device Management ---
447
446
 
448
- // Returns the generated AudioRoutesInfo data class
449
447
  fun getAudioDevices(): AudioRoutesInfo {
450
- audioManager = audioManager ?: appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
448
+ audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
451
449
  Log.e(TAG, "getAudioDevices: AudioManager is null or appContext is not set. Returning default.")
452
- return AudioRoutesInfo(emptyArray(), "Unknown") // Return default empty info
450
+ return AudioRoutesInfo(emptyArray(), "Unknown")
453
451
  }
454
452
  val devices = mutableListOf<String>()
455
453
  var currentRoute: String = "Unknown"
@@ -460,7 +458,7 @@ object CallEngine {
460
458
  when (device.type) {
461
459
  AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> {
462
460
  devices.add("Bluetooth")
463
- if (audioManager?.isBluetoothScoOn == true && !device.isSource) currentRoute = "Bluetooth" // check if output device
461
+ if (audioManager?.isBluetoothScoOn == true && !device.isSource) currentRoute = "Bluetooth"
464
462
  }
465
463
  AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET -> {
466
464
  devices.add("Headset")
@@ -472,8 +470,6 @@ object CallEngine {
472
470
  }
473
471
  AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> {
474
472
  devices.add("Earpiece")
475
- // Earpiece is active if speaker, bluetooth, and headset are off
476
- // This heuristic is tricky, relies on other routes being off
477
473
  if (audioManager?.isSpeakerphoneOn == false && audioManager?.isWiredHeadsetOn == false && audioManager?.isBluetoothScoOn == false && !device.isSource) {
478
474
  currentRoute = "Earpiece"
479
475
  }
@@ -482,17 +478,15 @@ object CallEngine {
482
478
  }
483
479
  }
484
480
  } else {
485
- // Fallback for older APIs. Detection is less reliable.
486
481
  devices.add("Speaker")
487
482
  devices.add("Earpiece")
488
483
  if (audioManager?.isSpeakerphoneOn == true) currentRoute = "Speaker"
489
- else currentRoute = "Earpiece" // Best guess for older API
484
+ else currentRoute = "Earpiece"
490
485
  }
491
486
 
492
487
  val distinctDevices = devices.distinct().toTypedArray()
493
488
 
494
- // Final check/refinement for currentRoute based on AudioManager flags
495
- if (currentRoute == "Unknown" || currentRoute == "Earpiece") { // If still unknown or defaulted to earpiece
489
+ if (currentRoute == "Unknown" || currentRoute == "Earpiece") {
496
490
  if (audioManager?.isBluetoothScoOn == true) {
497
491
  currentRoute = "Bluetooth"
498
492
  } else if (audioManager?.isSpeakerphoneOn == true) {
@@ -500,7 +494,7 @@ object CallEngine {
500
494
  } else if (audioManager?.isWiredHeadsetOn == true) {
501
495
  currentRoute = "Headset"
502
496
  } else {
503
- currentRoute = "Earpiece" // Assume earpiece as last resort
497
+ currentRoute = "Earpiece"
504
498
  }
505
499
  }
506
500
 
@@ -542,8 +536,6 @@ object CallEngine {
542
536
  Log.w(TAG, "Unknown audio route: $route. No action taken.")
543
537
  }
544
538
  }
545
- // Emit event immediately with the requested route.
546
- // The actual route change will be confirmed by onCallAudioStateChanged callback.
547
539
  emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, JSONObject().put("route", route))
548
540
  }
549
541
 
@@ -615,12 +607,12 @@ object CallEngine {
615
607
  Log.w(TAG, "Cannot emit AudioDevicesChanged: appContext is null.")
616
608
  return
617
609
  }
618
- val audioInfo = getAudioDevices() // Get AudioRoutesInfo data class
619
- val jsonPayload = JSONObject().apply { // Convert to JSONObject for emitEvent
610
+ val audioInfo = getAudioDevices()
611
+ val jsonPayload = JSONObject().apply {
620
612
  put("devices", JSONArray(audioInfo.devices.toList()))
621
613
  put("currentRoute", audioInfo.currentRoute)
622
614
  }
623
- emitEvent(CallEventType.AUDIO_DEVICES_CHANGED, jsonPayload) // Emit the full JSON object
615
+ emitEvent(CallEventType.AUDIO_DEVICES_CHANGED, jsonPayload)
624
616
  }
625
617
 
626
618
  // --- Call State Change Notification ---
@@ -669,12 +661,13 @@ object CallEngine {
669
661
  }
670
662
  }
671
663
 
664
+ // PhoneAccount registration is used for both INCOMING and OUTGOING calls when interacting with Telecom
672
665
  private fun registerPhoneAccount(context: Context) {
673
666
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
674
667
  val phoneAccountHandle = getPhoneAccountHandle(context)
675
668
  if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
676
669
  val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
677
- .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED or PhoneAccount.CAPABILITY_SUPPORTS_HOLD) // Resolved
670
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED or PhoneAccount.CAPABILITY_HOLD)
678
671
  .build()
679
672
  try {
680
673
  telecomManager.registerPhoneAccount(phoneAccount)
@@ -697,7 +690,6 @@ object CallEngine {
697
690
  }
698
691
 
699
692
  // --- Ringtone Management (for incoming calls, pre-Android S) ---
700
- // Can rely on default ringtone without adding custom file for incoming calls.
701
693
  fun playRingtone(context: Context) {
702
694
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
703
695
  Log.d(TAG, "playRingtone: Android S+ detected, system will handle ringtone via Telecom.")
@@ -730,15 +722,12 @@ object CallEngine {
730
722
  }
731
723
 
732
724
  // --- Ringback Tone Management (for outgoing calls) ---
733
- // Requires `res/raw/ringback_tone.mp3` for custom outgoing ringback.
734
725
  private fun startRingback() {
735
726
  if (ringbackPlayer != null && ringbackPlayer!!.isPlaying) {
736
727
  Log.d(TAG, "Ringback tone already playing.")
737
728
  return
738
729
  }
739
730
  try {
740
- // Ensure you have `res/raw/ringback_tone.mp3` in your project for this to work.
741
- // If you don't want a custom ringback, you can play a silent audio file or do nothing here.
742
731
  val ringbackUri = Uri.parse("android.resource://${appContext?.packageName}/raw/ringback_tone")
743
732
  ringbackPlayer = MediaPlayer.create(appContext, ringbackUri)
744
733
  if (ringbackPlayer == null) {
@@ -8,7 +8,7 @@ import android.content.Intent
8
8
  import android.os.Build
9
9
  import android.util.Log
10
10
  import android.os.IBinder
11
- import android.app.NotificationManager // Explicit import
11
+ import android.app.NotificationManager
12
12
 
13
13
  class CallForegroundService : Service() {
14
14
 
@@ -61,7 +61,7 @@ class CallForegroundService : Service() {
61
61
  NotificationManager.IMPORTANCE_LOW
62
62
  )
63
63
  channel.description = "Notification for ongoing calls"
64
- val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Corrected type inference and explicit import
64
+ val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
65
65
  manager.createNotificationChannel(channel)
66
66
  Log.d(TAG, "Foreground notification channel '$CHANNEL_ID' created/updated.")
67
67
  }
@@ -1,7 +1,7 @@
1
1
  package com.margelo.nitro.qusaieilouti99.callmanager
2
2
 
3
3
  import com.facebook.proguard.annotations.DoNotStrip
4
- import android.util.Log // Explicit import
4
+ import android.util.Log
5
5
  import org.json.JSONArray
6
6
  import org.json.JSONObject
7
7
 
@@ -10,7 +10,6 @@ class CallManager : HybridCallManagerSpec() {
10
10
 
11
11
  private val TAG = "CallManager"
12
12
 
13
- // Listener type now directly matches the generated Func_void_CallEventType_std__string
14
13
  private var currentListener: Func_void_CallEventType_std__string? = null
15
14
 
16
15
  override fun endCall(callId: String) {
@@ -27,14 +26,13 @@ class CallManager : HybridCallManagerSpec() {
27
26
  } ?: Log.e(TAG, "App context not set for silenceRingtone.")
28
27
  }
29
28
 
30
- // Return type changed to AudioRoutesInfo from String
31
29
  override fun getAudioDevices(): AudioRoutesInfo {
32
30
  Log.d(TAG, "getAudioDevices requested.")
33
31
  return CallEngine.getAppContext()?.let {
34
- CallEngine.getAudioDevices() // This now directly returns AudioRoutesInfo
32
+ CallEngine.getAudioDevices()
35
33
  } ?: run {
36
34
  Log.e(TAG, "App context not set for getAudioDevices. Returning empty AudioRoutesInfo.")
37
- AudioRoutesInfo(emptyArray(), "Unknown") // Return default empty data class
35
+ AudioRoutesInfo(emptyArray(), "Unknown")
38
36
  }
39
37
  }
40
38
 
@@ -52,29 +50,18 @@ class CallManager : HybridCallManagerSpec() {
52
50
  } ?: Log.e(TAG, "App context not set for keepAwake.")
53
51
  }
54
52
 
55
- // Corrected addListener signature to match generated HybridCallManagerSpec
56
53
  override fun addListener(
57
- listener: Func_void_CallEventType_std__string // Use generated type directly
58
- ): Func_void { // Return generated type
54
+ listener: (event: CallEventType, payload: String) -> Unit
55
+ ): () -> Unit {
59
56
  Log.d(TAG, "addListener called with listener: $listener")
60
- // Store the incoming Nitro-generated listener.
61
- // It already implements the (CallEventType, String) -> Unit interface.
62
- currentListener = listener
63
- CallEngine.setEventHandler(listener) // Pass it directly to CallEngine
64
-
65
- // Return a Nitro-generated Func_void for the remove function
66
- return Func_void_java {
67
- if (currentListener === listener) { // Compare against the original listener passed
68
- CallEngine.setEventHandler(null)
69
- currentListener = null
70
- Log.d(TAG, "Listener removed.")
71
- } else {
72
- Log.d(TAG, "Attempted to remove a listener that was not current.")
73
- }
57
+ // Wrap the listener in your event system
58
+ CallEngine.setEventHandler(listener)
59
+ return {
60
+ CallEngine.setEventHandler(null)
61
+ Log.d(TAG, "Listener removed.")
74
62
  }
75
63
  }
76
64
 
77
- // Removed callType param, now extracted from callData
78
65
  override fun startOutgoingCall(callId: String, callData: String) {
79
66
  Log.d(TAG, "startOutgoingCall requested: callId=$callId, callData=$callData")
80
67
  CallEngine.getAppContext()?.let {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",