@qusaieilouti99/call-manager 0.1.63 → 0.1.65

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,13 +48,11 @@ object CallEngine {
48
48
  private const val FOREGROUND_NOTIF_ID = 1001
49
49
 
50
50
  // Core context - initialized once and maintained
51
- // Improved context management with thread safety
52
51
  @Volatile
53
52
  private var appContext: Context? = null
54
53
  private val isInitialized = AtomicBoolean(false)
55
54
  private val initializationLock = Any()
56
55
 
57
-
58
56
  // Audio & Media
59
57
  private var ringtone: android.media.Ringtone? = null
60
58
  private var ringbackPlayer: MediaPlayer? = null
@@ -87,7 +85,7 @@ object CallEngine {
87
85
  fun onLockScreenBypassChanged(shouldBypass: Boolean)
88
86
  }
89
87
 
90
- // --- INITIALIZATION with better error handling ---
88
+ // --- INITIALIZATION - Fixed for better context management ---
91
89
  fun initialize(context: Context) {
92
90
  synchronized(initializationLock) {
93
91
  if (isInitialized.compareAndSet(false, true)) {
@@ -143,38 +141,79 @@ object CallEngine {
143
141
  }
144
142
  }
145
143
 
146
- // --- Audio Focus Management (Simplified) ---
144
+ // --- FIXED Audio Focus Management ---
147
145
  private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
148
146
  Log.d(TAG, "Audio focus changed: $focusChange")
149
147
  when (focusChange) {
150
- 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
+ }
151
154
  AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
155
+ Log.d(TAG, "Transient audio focus loss - temporary interruption")
152
156
  hasAudioFocus = false
153
157
  isSystemCallActive = true
154
158
  holdSystemCalls()
155
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
+ }
156
165
  AudioManager.AUDIOFOCUS_GAIN -> {
166
+ Log.d(TAG, "Audio focus gained")
157
167
  hasAudioFocus = true
158
168
  isSystemCallActive = false
169
+ // Delay resuming to avoid rapid hold/unhold cycles
159
170
  Handler(Looper.getMainLooper()).postDelayed({
160
171
  resumeSystemHeldCalls()
161
- }, 1000)
172
+ }, 500) // Reduced from 1000ms
173
+ }
174
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> {
175
+ Log.d(TAG, "Transient audio focus gained")
176
+ hasAudioFocus = true
162
177
  }
163
178
  }
164
179
  updateForegroundNotification()
165
180
  }
166
181
 
167
182
  private fun holdSystemCalls() {
168
- activeCalls.values.filter { it.state == CallState.ACTIVE }.forEach { call ->
169
- 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
170
197
  holdCallInternal(call.callId, heldBySystem = true)
198
+ } else {
199
+ Log.d(TAG, "Skipping hold for recently answered call: ${call.callId}")
171
200
  }
172
201
  }
173
202
  stopRingback()
174
203
  }
175
204
 
176
205
  private fun resumeSystemHeldCalls() {
177
- 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 ->
178
217
  unholdCallInternal(call.callId, resumedBySystem = true)
179
218
  }
180
219
  }
@@ -192,12 +231,13 @@ object CallEngine {
192
231
  .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
193
232
  .build()
194
233
  )
195
- .setOnAudioFocusChangeListener(audioFocusChangeListener)
234
+ .setOnAudioFocusChangeListener(audioFocusChangeListener, Handler(Looper.getMainLooper()))
235
+ .setAcceptsDelayedFocusGain(true) // Added this
196
236
  .build()
197
237
  }
198
238
  val result = audioManager?.requestAudioFocus(audioFocusRequest!!)
199
239
  hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
200
- Log.d(TAG, "Audio focus request result: $result")
240
+ Log.d(TAG, "Audio focus request result: $result (granted: $hasAudioFocus)")
201
241
  hasAudioFocus
202
242
  } else {
203
243
  @Suppress("DEPRECATION")
@@ -207,7 +247,7 @@ object CallEngine {
207
247
  AudioManager.AUDIOFOCUS_GAIN
208
248
  )
209
249
  hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
210
- Log.d(TAG, "Audio focus request result (legacy): $result")
250
+ Log.d(TAG, "Audio focus request result (legacy): $result (granted: $hasAudioFocus)")
211
251
  hasAudioFocus
212
252
  }
213
253
  }
@@ -345,7 +385,7 @@ object CallEngine {
345
385
 
346
386
  try {
347
387
  telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
348
- startForegroundService() // Fixed: Always start foreground service
388
+ startForegroundService()
349
389
  Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
350
390
  } catch (e: SecurityException) {
351
391
  Log.e(TAG, "SecurityException: Failed to report incoming call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
@@ -395,7 +435,7 @@ object CallEngine {
395
435
  val phoneAccountHandle = getPhoneAccountHandle()
396
436
  val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
397
437
 
398
- // 1) build a bundle of ONLY your own keys
438
+ // Build a bundle of ONLY your own keys
399
439
  val outgoingExtras = Bundle().apply {
400
440
  putString(MyConnectionService.EXTRA_CALL_ID, callId)
401
441
  putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
@@ -404,7 +444,7 @@ object CallEngine {
404
444
  metadata?.let { putString("metadata", it) }
405
445
  }
406
446
 
407
- // 2) wrap under the single Telecomhonored key
447
+ // Wrap under the single Telecom-honored key
408
448
  val extras = Bundle().apply {
409
449
  putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
410
450
  putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
@@ -414,8 +454,15 @@ object CallEngine {
414
454
  try {
415
455
  telecomManager.placeCall(addressUri, extras)
416
456
  startForegroundService()
417
- requestAudioFocus()
418
- 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
+
419
466
  bringAppToForeground()
420
467
  keepScreenAwake(true)
421
468
  setInitialAudioRoute(callType)
@@ -464,13 +511,13 @@ object CallEngine {
464
511
  registerPhoneAccount()
465
512
  requestAudioFocus()
466
513
  bringAppToForeground()
467
- startForegroundService() // Fixed: Start foreground service for JS-initiated calls
514
+ startForegroundService()
468
515
  keepScreenAwake(true)
469
516
  setInitialAudioRoute(callType)
470
517
  updateLockScreenBypass()
471
518
 
472
- // Emit call answered event with metadata
473
- emitCallAnsweredWithMetadata(callId)
519
+ // Emit outgoing call answered event with metadata for JS-initiated calls
520
+ emitOutgoingCallAnsweredWithMetadata(callId)
474
521
  }
475
522
 
476
523
  // --- Call Answer Management ---
@@ -484,6 +531,7 @@ object CallEngine {
484
531
  coreCallAnswered(callId, isLocalAnswer = true)
485
532
  }
486
533
 
534
+ // FIXED: Core Call Answered Method with proper audio focus handling
487
535
  private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
488
536
  val context = requireContext()
489
537
  Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
@@ -494,13 +542,27 @@ object CallEngine {
494
542
  return
495
543
  }
496
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
497
553
  stopRingtone()
498
554
  stopRingback()
499
555
  cancelIncomingCallUI()
500
- requestAudioFocus()
501
556
 
502
- activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
503
- 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
+ }
504
566
 
505
567
  if (!canMakeMultipleCalls) {
506
568
  activeCalls.filter { it.key != callId }.values.forEach { otherCall ->
@@ -511,18 +573,25 @@ object CallEngine {
511
573
  }
512
574
 
513
575
  bringAppToForeground()
514
- startForegroundService() // Fixed: Ensure foreground service is running
576
+ startForegroundService()
515
577
  keepScreenAwake(true)
516
578
  resetAudioMode()
517
579
  updateLockScreenBypass()
518
580
  updateForegroundNotification()
519
581
 
520
- // Emit with metadata
521
- 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
+ }
522
590
 
523
591
  Log.d(TAG, "Call $callId successfully answered and UI cleaned up")
524
592
  }
525
593
 
594
+ // For incoming calls (local answer)
526
595
  private fun emitCallAnsweredWithMetadata(callId: String) {
527
596
  val callInfo = activeCalls[callId] ?: return
528
597
  val metadata = callMetadata[callId]
@@ -543,6 +612,27 @@ object CallEngine {
543
612
  })
544
613
  }
545
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
+
546
636
  // --- Call Control Methods ---
547
637
  fun holdCall(callId: String) {
548
638
  holdCallInternal(callId, heldBySystem = false)
@@ -601,7 +691,7 @@ object CallEngine {
601
691
  return
602
692
  }
603
693
 
604
- // Fixed: Simplified audio focus check to prevent UNHELD FAILED
694
+ // FIXED: Simplified audio focus check to prevent UNHELD FAILED
605
695
  if (!hasAudioFocus && !resumedBySystem && !requestAudioFocus()) {
606
696
  Log.w(TAG, "Failed to get audio focus for unhold - but continuing anyway")
607
697
  // Don't emit UNHELD FAILED - just continue
@@ -1,2 +1,2 @@
1
- export type CallEventType = 'AUDIO_DEVICES_CHANGED' | 'AUDIO_ROUTE_CHANGED' | 'CALL_HELD' | 'CALL_UNHELD' | 'CALL_UNHOLD_FAILED' | 'CALL_MUTED' | 'CALL_UNMUTED' | 'CALL_ANSWERED' | 'CALL_REJECTED' | 'CALL_ENDED' | 'DTMF_TONE';
1
+ export type CallEventType = 'AUDIO_DEVICES_CHANGED' | 'AUDIO_ROUTE_CHANGED' | 'CALL_HELD' | 'CALL_UNHELD' | 'CALL_UNHOLD_FAILED' | 'CALL_MUTED' | 'CALL_UNMUTED' | 'CALL_ANSWERED' | 'OUTGOING_CALL_ANSWERED' | 'CALL_REJECTED' | 'CALL_ENDED' | 'DTMF_TONE';
2
2
  //# sourceMappingURL=CallEventType.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CallEventType.d.ts","sourceRoot":"","sources":["../../../src/CallEventType.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,aAAa,GACvB,uBAAuB,GACrB,qBAAqB,GACrB,WAAW,GACX,aAAa,GACb,oBAAoB,GACpB,YAAY,GACZ,cAAc,GACd,eAAe,GACf,eAAe,GACf,YAAY,GACZ,WAAW,CAAC"}
1
+ {"version":3,"file":"CallEventType.d.ts","sourceRoot":"","sources":["../../../src/CallEventType.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,aAAa,GACvB,uBAAuB,GACrB,qBAAqB,GACrB,WAAW,GACX,aAAa,GACb,oBAAoB,GACpB,YAAY,GACZ,cAAc,GACd,eAAe,GACf,wBAAwB,GACxB,eAAe,GACf,YAAY,GACZ,WAAW,CAAC"}
@@ -49,6 +49,7 @@ namespace margelo::nitro::qusaieilouti99_callmanager {
49
49
  static const auto fieldCALL_MUTED = clazz->getStaticField<JCallEventType>("CALL_MUTED");
50
50
  static const auto fieldCALL_UNMUTED = clazz->getStaticField<JCallEventType>("CALL_UNMUTED");
51
51
  static const auto fieldCALL_ANSWERED = clazz->getStaticField<JCallEventType>("CALL_ANSWERED");
52
+ static const auto fieldOUTGOING_CALL_ANSWERED = clazz->getStaticField<JCallEventType>("OUTGOING_CALL_ANSWERED");
52
53
  static const auto fieldCALL_REJECTED = clazz->getStaticField<JCallEventType>("CALL_REJECTED");
53
54
  static const auto fieldCALL_ENDED = clazz->getStaticField<JCallEventType>("CALL_ENDED");
54
55
  static const auto fieldDTMF_TONE = clazz->getStaticField<JCallEventType>("DTMF_TONE");
@@ -70,6 +71,8 @@ namespace margelo::nitro::qusaieilouti99_callmanager {
70
71
  return clazz->getStaticFieldValue(fieldCALL_UNMUTED);
71
72
  case CallEventType::CALL_ANSWERED:
72
73
  return clazz->getStaticFieldValue(fieldCALL_ANSWERED);
74
+ case CallEventType::OUTGOING_CALL_ANSWERED:
75
+ return clazz->getStaticFieldValue(fieldOUTGOING_CALL_ANSWERED);
73
76
  case CallEventType::CALL_REJECTED:
74
77
  return clazz->getStaticFieldValue(fieldCALL_REJECTED);
75
78
  case CallEventType::CALL_ENDED:
@@ -24,6 +24,7 @@ enum class CallEventType {
24
24
  CALL_MUTED,
25
25
  CALL_UNMUTED,
26
26
  CALL_ANSWERED,
27
+ OUTGOING_CALL_ANSWERED,
27
28
  CALL_REJECTED,
28
29
  CALL_ENDED,
29
30
  DTMF_TONE;
@@ -33,6 +33,8 @@ public extension CallEventType {
33
33
  self = .callUnmuted
34
34
  case "CALL_ANSWERED":
35
35
  self = .callAnswered
36
+ case "OUTGOING_CALL_ANSWERED":
37
+ self = .outgoingCallAnswered
36
38
  case "CALL_REJECTED":
37
39
  self = .callRejected
38
40
  case "CALL_ENDED":
@@ -65,6 +67,8 @@ public extension CallEventType {
65
67
  return "CALL_UNMUTED"
66
68
  case .callAnswered:
67
69
  return "CALL_ANSWERED"
70
+ case .outgoingCallAnswered:
71
+ return "OUTGOING_CALL_ANSWERED"
68
72
  case .callRejected:
69
73
  return "CALL_REJECTED"
70
74
  case .callEnded:
@@ -37,9 +37,10 @@ namespace margelo::nitro::qusaieilouti99_callmanager {
37
37
  CALL_MUTED SWIFT_NAME(callMuted) = 5,
38
38
  CALL_UNMUTED SWIFT_NAME(callUnmuted) = 6,
39
39
  CALL_ANSWERED SWIFT_NAME(callAnswered) = 7,
40
- CALL_REJECTED SWIFT_NAME(callRejected) = 8,
41
- CALL_ENDED SWIFT_NAME(callEnded) = 9,
42
- DTMF_TONE SWIFT_NAME(dtmfTone) = 10,
40
+ OUTGOING_CALL_ANSWERED SWIFT_NAME(outgoingCallAnswered) = 8,
41
+ CALL_REJECTED SWIFT_NAME(callRejected) = 9,
42
+ CALL_ENDED SWIFT_NAME(callEnded) = 10,
43
+ DTMF_TONE SWIFT_NAME(dtmfTone) = 11,
43
44
  } CLOSED_ENUM;
44
45
 
45
46
  } // namespace margelo::nitro::qusaieilouti99_callmanager
@@ -62,6 +63,7 @@ namespace margelo::nitro {
62
63
  case hashString("CALL_MUTED"): return CallEventType::CALL_MUTED;
63
64
  case hashString("CALL_UNMUTED"): return CallEventType::CALL_UNMUTED;
64
65
  case hashString("CALL_ANSWERED"): return CallEventType::CALL_ANSWERED;
66
+ case hashString("OUTGOING_CALL_ANSWERED"): return CallEventType::OUTGOING_CALL_ANSWERED;
65
67
  case hashString("CALL_REJECTED"): return CallEventType::CALL_REJECTED;
66
68
  case hashString("CALL_ENDED"): return CallEventType::CALL_ENDED;
67
69
  case hashString("DTMF_TONE"): return CallEventType::DTMF_TONE;
@@ -79,6 +81,7 @@ namespace margelo::nitro {
79
81
  case CallEventType::CALL_MUTED: return JSIConverter<std::string>::toJSI(runtime, "CALL_MUTED");
80
82
  case CallEventType::CALL_UNMUTED: return JSIConverter<std::string>::toJSI(runtime, "CALL_UNMUTED");
81
83
  case CallEventType::CALL_ANSWERED: return JSIConverter<std::string>::toJSI(runtime, "CALL_ANSWERED");
84
+ case CallEventType::OUTGOING_CALL_ANSWERED: return JSIConverter<std::string>::toJSI(runtime, "OUTGOING_CALL_ANSWERED");
82
85
  case CallEventType::CALL_REJECTED: return JSIConverter<std::string>::toJSI(runtime, "CALL_REJECTED");
83
86
  case CallEventType::CALL_ENDED: return JSIConverter<std::string>::toJSI(runtime, "CALL_ENDED");
84
87
  case CallEventType::DTMF_TONE: return JSIConverter<std::string>::toJSI(runtime, "DTMF_TONE");
@@ -101,6 +104,7 @@ namespace margelo::nitro {
101
104
  case hashString("CALL_MUTED"):
102
105
  case hashString("CALL_UNMUTED"):
103
106
  case hashString("CALL_ANSWERED"):
107
+ case hashString("OUTGOING_CALL_ANSWERED"):
104
108
  case hashString("CALL_REJECTED"):
105
109
  case hashString("CALL_ENDED"):
106
110
  case hashString("DTMF_TONE"):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -8,6 +8,7 @@ export type CallEventType =
8
8
  | 'CALL_MUTED'
9
9
  | 'CALL_UNMUTED'
10
10
  | 'CALL_ANSWERED'
11
+ | 'OUTGOING_CALL_ANSWERED'
11
12
  | 'CALL_REJECTED'
12
13
  | 'CALL_ENDED'
13
14
  | 'DTMF_TONE'; // ADD THIS LINE IF YOU NEED DTMF EVENTS