@qusaieilouti99/call-manager 0.1.62 → 0.1.64

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.
@@ -48,8 +48,10 @@ object CallEngine {
48
48
  private const val FOREGROUND_NOTIF_ID = 1001
49
49
 
50
50
  // Core context - initialized once and maintained
51
+ @Volatile
51
52
  private var appContext: Context? = null
52
- private var isInitialized = AtomicBoolean(false)
53
+ private val isInitialized = AtomicBoolean(false)
54
+ private val initializationLock = Any()
53
55
 
54
56
  // Audio & Media
55
57
  private var ringtone: android.media.Ringtone? = null
@@ -83,16 +85,25 @@ object CallEngine {
83
85
  fun onLockScreenBypassChanged(shouldBypass: Boolean)
84
86
  }
85
87
 
86
- // --- INITIALIZATION - Fix for context management ---
88
+ // --- INITIALIZATION - Fixed for better context management ---
87
89
  fun initialize(context: Context) {
88
- if (isInitialized.compareAndSet(false, true)) {
89
- appContext = context.applicationContext
90
- audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
91
- Log.d(TAG, "CallEngine initialized with context")
92
-
93
- // Initialize foreground service if needed
94
- if (isCallActive()) {
95
- startForegroundService()
90
+ synchronized(initializationLock) {
91
+ if (isInitialized.compareAndSet(false, true)) {
92
+ appContext = context.applicationContext
93
+ audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
94
+ Log.d(TAG, "CallEngine initialized successfully with context: ${context.javaClass.simpleName}")
95
+
96
+ // Verify critical services are available
97
+ if (audioManager == null) {
98
+ Log.w(TAG, "AudioManager is null after initialization")
99
+ }
100
+
101
+ // Initialize foreground service if needed
102
+ if (isCallActive()) {
103
+ startForegroundService()
104
+ }
105
+ } else {
106
+ Log.d(TAG, "CallEngine already initialized, skipping")
96
107
  }
97
108
  }
98
109
  }
@@ -100,7 +111,9 @@ object CallEngine {
100
111
  fun isInitialized(): Boolean = isInitialized.get()
101
112
 
102
113
  private fun requireContext(): Context {
103
- return appContext ?: throw IllegalStateException("CallEngine not initialized. Call initialize() first.")
114
+ return appContext ?: throw IllegalStateException(
115
+ "CallEngine not initialized. Ensure CallEngine.initialize(context) is called in Application.onCreate() before any module usage."
116
+ )
104
117
  }
105
118
 
106
119
  // --- Event System ---
@@ -128,38 +141,79 @@ object CallEngine {
128
141
  }
129
142
  }
130
143
 
131
- // --- Audio Focus Management (Simplified) ---
144
+ // --- FIXED Audio Focus Management ---
132
145
  private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
133
146
  Log.d(TAG, "Audio focus changed: $focusChange")
134
147
  when (focusChange) {
135
- AudioManager.AUDIOFOCUS_LOSS,
148
+ AudioManager.AUDIOFOCUS_LOSS -> {
149
+ Log.d(TAG, "Permanent audio focus loss - another app took focus")
150
+ hasAudioFocus = false
151
+ isSystemCallActive = true
152
+ holdSystemCalls()
153
+ }
136
154
  AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
155
+ Log.d(TAG, "Transient audio focus loss - temporary interruption")
137
156
  hasAudioFocus = false
138
157
  isSystemCallActive = true
139
158
  holdSystemCalls()
140
159
  }
160
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
161
+ Log.d(TAG, "Audio focus loss with ducking - lowering volume but keeping active")
162
+ // Don't hold the call for ducking, just lower volume
163
+ hasAudioFocus = false
164
+ }
141
165
  AudioManager.AUDIOFOCUS_GAIN -> {
166
+ Log.d(TAG, "Audio focus gained")
142
167
  hasAudioFocus = true
143
168
  isSystemCallActive = false
169
+ // Delay resuming to avoid rapid hold/unhold cycles
144
170
  Handler(Looper.getMainLooper()).postDelayed({
145
171
  resumeSystemHeldCalls()
146
- }, 1000)
172
+ }, 500) // Reduced from 1000ms
173
+ }
174
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> {
175
+ Log.d(TAG, "Transient audio focus gained")
176
+ hasAudioFocus = true
147
177
  }
148
178
  }
149
179
  updateForegroundNotification()
150
180
  }
151
181
 
152
182
  private fun holdSystemCalls() {
153
- activeCalls.values.filter { it.state == CallState.ACTIVE }.forEach { call ->
154
- if (!call.wasHeldBySystem) {
183
+ val callsToHold = activeCalls.values.filter {
184
+ it.state == CallState.ACTIVE && !it.wasHeldBySystem
185
+ }
186
+
187
+ if (callsToHold.isEmpty()) {
188
+ Log.d(TAG, "No active calls to hold due to audio focus loss")
189
+ return
190
+ }
191
+
192
+ Log.d(TAG, "Holding ${callsToHold.size} calls due to audio focus loss")
193
+ callsToHold.forEach { call ->
194
+ // Add a small delay to prevent holding immediately after answering
195
+ val timeSinceAnswer = System.currentTimeMillis() - call.timestamp
196
+ if (timeSinceAnswer > 2000) { // Only hold if call has been active for 2+ seconds
155
197
  holdCallInternal(call.callId, heldBySystem = true)
198
+ } else {
199
+ Log.d(TAG, "Skipping hold for recently answered call: ${call.callId}")
156
200
  }
157
201
  }
158
202
  stopRingback()
159
203
  }
160
204
 
161
205
  private fun resumeSystemHeldCalls() {
162
- activeCalls.values.filter { it.state == CallState.HELD && it.wasHeldBySystem }.forEach { call ->
206
+ val callsToResume = activeCalls.values.filter {
207
+ it.state == CallState.HELD && it.wasHeldBySystem
208
+ }
209
+
210
+ if (callsToResume.isEmpty()) {
211
+ Log.d(TAG, "No system-held calls to resume")
212
+ return
213
+ }
214
+
215
+ Log.d(TAG, "Resuming ${callsToResume.size} system-held calls")
216
+ callsToResume.forEach { call ->
163
217
  unholdCallInternal(call.callId, resumedBySystem = true)
164
218
  }
165
219
  }
@@ -177,12 +231,13 @@ object CallEngine {
177
231
  .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
178
232
  .build()
179
233
  )
180
- .setOnAudioFocusChangeListener(audioFocusChangeListener)
234
+ .setOnAudioFocusChangeListener(audioFocusChangeListener, Handler(Looper.getMainLooper()))
235
+ .setAcceptsDelayedFocusGain(true) // Added this
181
236
  .build()
182
237
  }
183
238
  val result = audioManager?.requestAudioFocus(audioFocusRequest!!)
184
239
  hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
185
- Log.d(TAG, "Audio focus request result: $result")
240
+ Log.d(TAG, "Audio focus request result: $result (granted: $hasAudioFocus)")
186
241
  hasAudioFocus
187
242
  } else {
188
243
  @Suppress("DEPRECATION")
@@ -192,7 +247,7 @@ object CallEngine {
192
247
  AudioManager.AUDIOFOCUS_GAIN
193
248
  )
194
249
  hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
195
- Log.d(TAG, "Audio focus request result (legacy): $result")
250
+ Log.d(TAG, "Audio focus request result (legacy): $result (granted: $hasAudioFocus)")
196
251
  hasAudioFocus
197
252
  }
198
253
  }
@@ -330,7 +385,7 @@ object CallEngine {
330
385
 
331
386
  try {
332
387
  telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
333
- startForegroundService() // Fixed: Always start foreground service
388
+ startForegroundService()
334
389
  Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
335
390
  } catch (e: SecurityException) {
336
391
  Log.e(TAG, "SecurityException: Failed to report incoming call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
@@ -380,7 +435,7 @@ object CallEngine {
380
435
  val phoneAccountHandle = getPhoneAccountHandle()
381
436
  val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
382
437
 
383
- // 1) build a bundle of ONLY your own keys
438
+ // Build a bundle of ONLY your own keys
384
439
  val outgoingExtras = Bundle().apply {
385
440
  putString(MyConnectionService.EXTRA_CALL_ID, callId)
386
441
  putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
@@ -389,7 +444,7 @@ object CallEngine {
389
444
  metadata?.let { putString("metadata", it) }
390
445
  }
391
446
 
392
- // 2) wrap under the single Telecomhonored key
447
+ // Wrap under the single Telecom-honored key
393
448
  val extras = Bundle().apply {
394
449
  putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
395
450
  putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
@@ -399,8 +454,15 @@ object CallEngine {
399
454
  try {
400
455
  telecomManager.placeCall(addressUri, extras)
401
456
  startForegroundService()
402
- requestAudioFocus()
403
- startRingback()
457
+
458
+ // FIXED: Request audio focus BEFORE starting ringback
459
+ val audioFocusGranted = requestAudioFocus()
460
+ if (audioFocusGranted) {
461
+ startRingback()
462
+ } else {
463
+ Log.w(TAG, "Audio focus not granted for outgoing call, skipping ringback")
464
+ }
465
+
404
466
  bringAppToForeground()
405
467
  keepScreenAwake(true)
406
468
  setInitialAudioRoute(callType)
@@ -449,13 +511,13 @@ object CallEngine {
449
511
  registerPhoneAccount()
450
512
  requestAudioFocus()
451
513
  bringAppToForeground()
452
- startForegroundService() // Fixed: Start foreground service for JS-initiated calls
514
+ startForegroundService()
453
515
  keepScreenAwake(true)
454
516
  setInitialAudioRoute(callType)
455
517
  updateLockScreenBypass()
456
518
 
457
- // Emit call answered event with metadata
458
- emitCallAnsweredWithMetadata(callId)
519
+ // Emit outgoing call answered event with metadata for JS-initiated calls
520
+ emitOutgoingCallAnsweredWithMetadata(callId)
459
521
  }
460
522
 
461
523
  // --- Call Answer Management ---
@@ -469,6 +531,7 @@ object CallEngine {
469
531
  coreCallAnswered(callId, isLocalAnswer = true)
470
532
  }
471
533
 
534
+ // FIXED: Core Call Answered Method with proper audio focus handling
472
535
  private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
473
536
  val context = requireContext()
474
537
  Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
@@ -479,13 +542,27 @@ object CallEngine {
479
542
  return
480
543
  }
481
544
 
545
+ // FIXED: Request audio focus FIRST, before stopping media
546
+ val audioFocusGranted = requestAudioFocus()
547
+ if (!audioFocusGranted) {
548
+ Log.w(TAG, "Failed to get audio focus for call $callId, but continuing...")
549
+ // Don't fail the call, but warn about audio issues
550
+ }
551
+
552
+ // Stop media AFTER getting audio focus
482
553
  stopRingtone()
483
554
  stopRingback()
484
555
  cancelIncomingCallUI()
485
- requestAudioFocus()
486
556
 
487
- activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
488
- currentCallId = callId
557
+ // FIXED: Only set call to ACTIVE if we have audio focus OR if it's a remote answer
558
+ if (audioFocusGranted || !isLocalAnswer) {
559
+ activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
560
+ currentCallId = callId
561
+ Log.d(TAG, "Call $callId set to ACTIVE state")
562
+ } else {
563
+ Log.w(TAG, "Call $callId not set to ACTIVE due to audio focus failure")
564
+ return
565
+ }
489
566
 
490
567
  if (!canMakeMultipleCalls) {
491
568
  activeCalls.filter { it.key != callId }.values.forEach { otherCall ->
@@ -496,18 +573,25 @@ object CallEngine {
496
573
  }
497
574
 
498
575
  bringAppToForeground()
499
- startForegroundService() // Fixed: Ensure foreground service is running
576
+ startForegroundService()
500
577
  keepScreenAwake(true)
501
578
  resetAudioMode()
502
579
  updateLockScreenBypass()
503
580
  updateForegroundNotification()
504
581
 
505
- // Emit with metadata
506
- emitCallAnsweredWithMetadata(callId)
582
+ // FIXED: Emit different events based on call direction
583
+ if (isLocalAnswer) {
584
+ // This is for incoming calls - user answered locally
585
+ emitCallAnsweredWithMetadata(callId)
586
+ } else {
587
+ // This is for outgoing calls - remote party answered
588
+ emitOutgoingCallAnsweredWithMetadata(callId)
589
+ }
507
590
 
508
591
  Log.d(TAG, "Call $callId successfully answered and UI cleaned up")
509
592
  }
510
593
 
594
+ // For incoming calls (local answer)
511
595
  private fun emitCallAnsweredWithMetadata(callId: String) {
512
596
  val callInfo = activeCalls[callId] ?: return
513
597
  val metadata = callMetadata[callId]
@@ -528,6 +612,27 @@ object CallEngine {
528
612
  })
529
613
  }
530
614
 
615
+ // For outgoing calls (remote answer)
616
+ private fun emitOutgoingCallAnsweredWithMetadata(callId: String) {
617
+ val callInfo = activeCalls[callId] ?: return
618
+ val metadata = callMetadata[callId]
619
+
620
+ emitEvent(CallEventType.OUTGOING_CALL_ANSWERED, JSONObject().apply {
621
+ put("callId", callId)
622
+ put("callType", callInfo.callType)
623
+ put("displayName", callInfo.displayName)
624
+ callInfo.pictureUrl?.let { put("pictureUrl", it) }
625
+ metadata?.let {
626
+ try {
627
+ put("metadata", JSONObject(it))
628
+ } catch (e: Exception) {
629
+ Log.w(TAG, "Invalid metadata JSON for callId: $callId", e)
630
+ put("metadata", it) // fallback to string
631
+ }
632
+ }
633
+ })
634
+ }
635
+
531
636
  // --- Call Control Methods ---
532
637
  fun holdCall(callId: String) {
533
638
  holdCallInternal(callId, heldBySystem = false)
@@ -586,7 +691,7 @@ object CallEngine {
586
691
  return
587
692
  }
588
693
 
589
- // Fixed: Simplified audio focus check to prevent UNHELD FAILED
694
+ // FIXED: Simplified audio focus check to prevent UNHELD FAILED
590
695
  if (!hasAudioFocus && !resumedBySystem && !requestAudioFocus()) {
591
696
  Log.w(TAG, "Failed to get audio focus for unhold - but continuing anyway")
592
697
  // Don't emit UNHELD FAILED - just continue
@@ -1,48 +1,26 @@
1
1
  package com.margelo.nitro.qusaieilouti99.callmanager
2
2
 
3
3
  import android.util.Log
4
- import com.facebook.react.bridge.ReactApplicationContext
5
4
  import com.facebook.proguard.annotations.DoNotStrip
6
5
 
7
6
  @DoNotStrip
8
- class CallManager : HybridCallManagerSpec() { // Match the parameterless constructor
7
+ class CallManager : HybridCallManagerSpec() {
9
8
 
10
9
  private val TAG = "CallManager"
11
10
 
12
- // This method ensures CallEngine has a valid context.
13
- // It primarily relies on MainApplication.onCreate, but provides a fallback.
11
+ // Simplified approach - rely on proper Application.onCreate() initialization
12
+ // Remove all fallback context access attempts that don't work with Nitro modules
14
13
  private fun ensureInitialized() {
15
14
  if (!CallEngine.isInitialized()) {
16
- // This log should ideally NOT be seen frequently in production.
17
- // If it appears, it means CallEngine wasn't initialized by MainApplication.
18
- Log.e(TAG, "CallEngine not initialized! Attempting late initialization from CallManager.")
19
- try {
20
- // For React Native modules (including JSI/Nitro ones),
21
- // ReactApplicationContext.getCurrentApplicationContext() provides the
22
- // ReactApplicationContext once the bridge is set up. From there, we get the Application context.
23
- val reactContext = ReactApplicationContext.getCurrentApplicationContext()
24
-
25
- if (reactContext != null) {
26
- val appContext = reactContext.applicationContext
27
- if (appContext != null) {
28
- CallEngine.initialize(appContext)
29
- Log.d(TAG, "CallEngine successfully (late) initialized via ReactApplicationContext.")
30
- } else {
31
- Log.e(TAG, "Error: reactContext.applicationContext is null. Cannot late-initialize CallEngine.")
32
- throw IllegalStateException("CallEngine initialization failed: Application context is null.")
33
- }
34
- } else {
35
- Log.e(TAG, "Error: ReactApplicationContext.getCurrentApplicationContext() returned null. Cannot late-initialize CallEngine.")
36
- throw IllegalStateException("CallEngine initialization failed: ReactApplicationContext not available.")
37
- }
38
- } catch (e: Exception) {
39
- Log.e(TAG, "Exception during CallEngine late initialization: ${e.message}", e)
40
- throw IllegalStateException("CallEngine fatal error during initialization: ${e.message}", e)
41
- }
15
+ Log.e(TAG, "CallEngine not initialized! This should not happen if Application.onCreate() was called properly.")
16
+ throw IllegalStateException(
17
+ "CallEngine must be initialized in Application.onCreate(). " +
18
+ "Make sure MainApplication.onCreate() calls CallEngine.initialize(this) before any native calls."
19
+ )
42
20
  }
43
21
  }
44
22
 
45
- // --- All your overridden methods must call ensureInitialized() first ---
23
+ // --- All methods must call ensureInitialized() first ---
46
24
 
47
25
  override fun endCall(callId: String): Unit {
48
26
  Log.d(TAG, "endCall requested for callId: $callId")
@@ -81,7 +59,7 @@ class CallManager : HybridCallManagerSpec() { // Match the parameterless constru
81
59
  }
82
60
 
83
61
  override fun addListener(listener: (event: CallEventType, payload: String) -> Unit): () -> Unit {
84
- Log.d(TAG, "addListener called with listener: $listener")
62
+ Log.d(TAG, "addListener called")
85
63
  ensureInitialized()
86
64
  CallEngine.setEventHandler(listener)
87
65
  return {
@@ -93,7 +71,6 @@ class CallManager : HybridCallManagerSpec() { // Match the parameterless constru
93
71
  override fun startOutgoingCall(callId: String, callType: String, targetName: String, metadata: String?): Unit {
94
72
  Log.d(TAG, "startOutgoingCall requested: callId=$callId, callType=$callType, targetName=$targetName")
95
73
  ensureInitialized()
96
- // IMPORTANT: The Bundle wrapping fix for TelecomManager must still be in CallEngine.startOutgoingCall
97
74
  CallEngine.startOutgoingCall(callId, callType, targetName, metadata)
98
75
  }
99
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",