@qusaieilouti99/call-manager 0.1.64 → 0.1.66

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.
@@ -44,8 +44,6 @@ object CallEngine {
44
44
  private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
45
45
  private const val NOTIF_CHANNEL_ID = "incoming_call_channel"
46
46
  private const val NOTIF_ID = 2001
47
- private const val FOREGROUND_CHANNEL_ID = "call_foreground_channel"
48
- private const val FOREGROUND_NOTIF_ID = 1001
49
47
 
50
48
  // Core context - initialized once and maintained
51
49
  @Volatile
@@ -53,12 +51,13 @@ object CallEngine {
53
51
  private val isInitialized = AtomicBoolean(false)
54
52
  private val initializationLock = Any()
55
53
 
56
- // Audio & Media
54
+ // Audio & Media - SIMPLIFIED
57
55
  private var ringtone: android.media.Ringtone? = null
58
56
  private var ringbackPlayer: MediaPlayer? = null
59
57
  private var audioManager: AudioManager? = null
60
58
  private var wakeLock: PowerManager.WakeLock? = null
61
59
  private var audioFocusRequest: AudioFocusRequest? = null
60
+ private var hasAudioFocus: Boolean = false
62
61
 
63
62
  // Call State Management
64
63
  private val activeCalls = ConcurrentHashMap<String, CallInfo>()
@@ -68,10 +67,8 @@ object CallEngine {
68
67
  private var currentCallId: String? = null
69
68
  private var canMakeMultipleCalls: Boolean = false
70
69
 
71
- // Audio State Tracking
70
+ // Audio State Tracking - SIMPLIFIED
72
71
  private var lastAudioRoutesInfo: AudioRoutesInfo? = null
73
- private var hasAudioFocus: Boolean = false
74
- private var isSystemCallActive: Boolean = false
75
72
 
76
73
  // Lock Screen Bypass
77
74
  private var lockScreenBypassActive = false
@@ -85,25 +82,17 @@ object CallEngine {
85
82
  fun onLockScreenBypassChanged(shouldBypass: Boolean)
86
83
  }
87
84
 
88
- // --- INITIALIZATION - Fixed for better context management ---
85
+ // --- INITIALIZATION ---
89
86
  fun initialize(context: Context) {
90
87
  synchronized(initializationLock) {
91
88
  if (isInitialized.compareAndSet(false, true)) {
92
89
  appContext = context.applicationContext
93
90
  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
- }
91
+ Log.d(TAG, "CallEngine initialized successfully")
100
92
 
101
- // Initialize foreground service if needed
102
93
  if (isCallActive()) {
103
94
  startForegroundService()
104
95
  }
105
- } else {
106
- Log.d(TAG, "CallEngine already initialized, skipping")
107
96
  }
108
97
  }
109
98
  }
@@ -112,7 +101,7 @@ object CallEngine {
112
101
 
113
102
  private fun requireContext(): Context {
114
103
  return appContext ?: throw IllegalStateException(
115
- "CallEngine not initialized. Ensure CallEngine.initialize(context) is called in Application.onCreate() before any module usage."
104
+ "CallEngine not initialized. Call initialize() in Application.onCreate()"
116
105
  )
117
106
  }
118
107
 
@@ -129,93 +118,41 @@ object CallEngine {
129
118
  }
130
119
  }
131
120
 
132
- // Made public for MyConnection
133
121
  fun emitEvent(type: CallEventType, data: JSONObject) {
134
- Log.d(TAG, "Emitting event: $type, data: $data")
122
+ Log.d(TAG, "Emitting event: $type")
135
123
  val dataString = data.toString()
136
124
  if (eventHandler != null) {
137
125
  eventHandler?.invoke(type, dataString)
138
126
  } else {
139
- Log.d(TAG, "No event handler registered, caching event: $type")
127
+ Log.d(TAG, "No event handler, caching event: $type")
140
128
  cachedEvents.add(Pair(type, dataString))
141
129
  }
142
130
  }
143
131
 
144
- // --- FIXED Audio Focus Management ---
132
+ // --- SIMPLIFIED Audio Focus Management ---
145
133
  private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
146
134
  Log.d(TAG, "Audio focus changed: $focusChange")
147
135
  when (focusChange) {
148
136
  AudioManager.AUDIOFOCUS_LOSS -> {
149
- Log.d(TAG, "Permanent audio focus loss - another app took focus")
150
- hasAudioFocus = false
151
- isSystemCallActive = true
152
- holdSystemCalls()
153
- }
154
- AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
155
- Log.d(TAG, "Transient audio focus loss - temporary interruption")
156
- hasAudioFocus = false
157
- isSystemCallActive = true
158
- holdSystemCalls()
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
137
+ // Only hold calls on PERMANENT audio focus loss (like system calls)
138
+ Log.d(TAG, "Permanent audio focus loss - holding calls")
163
139
  hasAudioFocus = false
140
+ holdAllActiveCalls(heldBySystem = true)
164
141
  }
165
142
  AudioManager.AUDIOFOCUS_GAIN -> {
166
- Log.d(TAG, "Audio focus gained")
143
+ Log.d(TAG, "Audio focus gained - resuming held calls")
167
144
  hasAudioFocus = true
168
- isSystemCallActive = false
169
- // Delay resuming to avoid rapid hold/unhold cycles
170
145
  Handler(Looper.getMainLooper()).postDelayed({
171
146
  resumeSystemHeldCalls()
172
- }, 500) // Reduced from 1000ms
173
- }
174
- AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> {
175
- Log.d(TAG, "Transient audio focus gained")
176
- hasAudioFocus = true
147
+ }, 1000)
177
148
  }
178
- }
179
- updateForegroundNotification()
180
- }
181
-
182
- private fun holdSystemCalls() {
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
197
- holdCallInternal(call.callId, heldBySystem = true)
198
- } else {
199
- Log.d(TAG, "Skipping hold for recently answered call: ${call.callId}")
149
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT,
150
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
151
+ // Don't hold calls for transient losses - just note we lost focus
152
+ Log.d(TAG, "Transient audio focus loss - keeping calls active")
153
+ hasAudioFocus = false
200
154
  }
201
155
  }
202
- stopRingback()
203
- }
204
-
205
- private fun resumeSystemHeldCalls() {
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 ->
217
- unholdCallInternal(call.callId, resumedBySystem = true)
218
- }
219
156
  }
220
157
 
221
158
  private fun requestAudioFocus(): Boolean {
@@ -231,8 +168,8 @@ object CallEngine {
231
168
  .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
232
169
  .build()
233
170
  )
234
- .setOnAudioFocusChangeListener(audioFocusChangeListener, Handler(Looper.getMainLooper()))
235
- .setAcceptsDelayedFocusGain(true) // Added this
171
+ .setOnAudioFocusChangeListener(audioFocusChangeListener)
172
+ .setAcceptsDelayedFocusGain(true)
236
173
  .build()
237
174
  }
238
175
  val result = audioManager?.requestAudioFocus(audioFocusRequest!!)
@@ -247,7 +184,6 @@ object CallEngine {
247
184
  AudioManager.AUDIOFOCUS_GAIN
248
185
  )
249
186
  hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
250
- Log.d(TAG, "Audio focus request result (legacy): $result (granted: $hasAudioFocus)")
251
187
  hasAudioFocus
252
188
  }
253
189
  }
@@ -267,6 +203,18 @@ object CallEngine {
267
203
  Log.d(TAG, "Audio focus abandoned")
268
204
  }
269
205
 
206
+ private fun holdAllActiveCalls(heldBySystem: Boolean) {
207
+ activeCalls.values.filter { it.state == CallState.ACTIVE }.forEach { call ->
208
+ holdCallInternal(call.callId, heldBySystem = heldBySystem)
209
+ }
210
+ }
211
+
212
+ private fun resumeSystemHeldCalls() {
213
+ activeCalls.values.filter { it.state == CallState.HELD && it.wasHeldBySystem }.forEach { call ->
214
+ unholdCallInternal(call.callId, resumedBySystem = true)
215
+ }
216
+ }
217
+
270
218
  // --- Lock Screen Bypass Management ---
271
219
  fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
272
220
  lockScreenBypassCallbacks.add(callback)
@@ -296,12 +244,12 @@ object CallEngine {
296
244
  // --- Telecom Connection Management ---
297
245
  fun addTelecomConnection(callId: String, connection: Connection) {
298
246
  telecomConnections[callId] = connection
299
- Log.d(TAG, "Added Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
247
+ Log.d(TAG, "Added Telecom Connection for callId: $callId")
300
248
  }
301
249
 
302
250
  fun removeTelecomConnection(callId: String) {
303
251
  telecomConnections.remove(callId)?.let {
304
- Log.d(TAG, "Removed Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
252
+ Log.d(TAG, "Removed Telecom Connection for callId: $callId")
305
253
  }
306
254
  }
307
255
 
@@ -319,9 +267,7 @@ object CallEngine {
319
267
  calls.forEach {
320
268
  jsonArray.put(it.toJsonObject())
321
269
  }
322
- val result = jsonArray.toString()
323
- Log.d(TAG, "Current call state: $result")
324
- return result
270
+ return jsonArray.toString()
325
271
  }
326
272
 
327
273
  // --- Incoming Call Management ---
@@ -368,7 +314,7 @@ object CallEngine {
368
314
 
369
315
  activeCalls[callId] = CallInfo(callId, callType, displayName, pictureUrl, CallState.INCOMING)
370
316
  currentCallId = callId
371
- Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING, callType: $callType")
317
+ Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING")
372
318
 
373
319
  showIncomingCallUI(callId, displayName, callType)
374
320
  registerPhoneAccount()
@@ -387,9 +333,6 @@ object CallEngine {
387
333
  telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
388
334
  startForegroundService()
389
335
  Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
390
- } catch (e: SecurityException) {
391
- Log.e(TAG, "SecurityException: Failed to report incoming call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
392
- endCallInternal(callId)
393
336
  } catch (e: Exception) {
394
337
  Log.e(TAG, "Failed to report incoming call: ${e.message}", e)
395
338
  endCallInternal(callId)
@@ -435,7 +378,6 @@ object CallEngine {
435
378
  val phoneAccountHandle = getPhoneAccountHandle()
436
379
  val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
437
380
 
438
- // Build a bundle of ONLY your own keys
439
381
  val outgoingExtras = Bundle().apply {
440
382
  putString(MyConnectionService.EXTRA_CALL_ID, callId)
441
383
  putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
@@ -444,7 +386,6 @@ object CallEngine {
444
386
  metadata?.let { putString("metadata", it) }
445
387
  }
446
388
 
447
- // Wrap under the single Telecom-honored key
448
389
  val extras = Bundle().apply {
449
390
  putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
450
391
  putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
@@ -455,21 +396,14 @@ object CallEngine {
455
396
  telecomManager.placeCall(addressUri, extras)
456
397
  startForegroundService()
457
398
 
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
- }
399
+ // SIMPLIFIED: Request audio focus and start ringback
400
+ requestAudioFocus()
401
+ startRingback()
465
402
 
466
403
  bringAppToForeground()
467
404
  keepScreenAwake(true)
468
405
  setInitialAudioRoute(callType)
469
406
  Log.d(TAG, "Successfully reported outgoing call to TelecomManager")
470
- } catch (e: SecurityException) {
471
- Log.e(TAG, "SecurityException placing outgoing call: ${e.message}", e)
472
- endCallInternal(callId)
473
407
  } catch (e: Exception) {
474
408
  Log.e(TAG, "Failed to start outgoing call: ${e.message}", e)
475
409
  endCallInternal(callId)
@@ -478,14 +412,12 @@ object CallEngine {
478
412
  updateLockScreenBypass()
479
413
  }
480
414
 
481
- // Fixed: Start call as active (not dialing) with foreground service
482
415
  fun startCall(
483
416
  callId: String,
484
417
  callType: String,
485
418
  targetName: String,
486
419
  metadata: String? = null
487
420
  ) {
488
- val context = requireContext()
489
421
  Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
490
422
 
491
423
  metadata?.let { callMetadata[callId] = it }
@@ -506,7 +438,7 @@ object CallEngine {
506
438
  // Start directly as active call
507
439
  activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
508
440
  currentCallId = callId
509
- Log.d(TAG, "Call $callId started as ACTIVE, callType: $callType")
441
+ Log.d(TAG, "Call $callId started as ACTIVE")
510
442
 
511
443
  registerPhoneAccount()
512
444
  requestAudioFocus()
@@ -516,11 +448,11 @@ object CallEngine {
516
448
  setInitialAudioRoute(callType)
517
449
  updateLockScreenBypass()
518
450
 
519
- // Emit outgoing call answered event with metadata for JS-initiated calls
451
+ // Emit outgoing call answered event
520
452
  emitOutgoingCallAnsweredWithMetadata(callId)
521
453
  }
522
454
 
523
- // --- Call Answer Management ---
455
+ // --- Call Answer Management - SIMPLIFIED ---
524
456
  fun callAnsweredFromJS(callId: String) {
525
457
  Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
526
458
  coreCallAnswered(callId, isLocalAnswer = false)
@@ -531,9 +463,8 @@ object CallEngine {
531
463
  coreCallAnswered(callId, isLocalAnswer = true)
532
464
  }
533
465
 
534
- // FIXED: Core Call Answered Method with proper audio focus handling
466
+ // SIMPLIFIED: Always succeed call answer, handle audio focus gracefully
535
467
  private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
536
- val context = requireContext()
537
468
  Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
538
469
 
539
470
  val callInfo = activeCalls[callId]
@@ -542,27 +473,18 @@ object CallEngine {
542
473
  return
543
474
  }
544
475
 
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
- }
476
+ // ALWAYS set call to ACTIVE - don't fail due to audio focus
477
+ activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
478
+ currentCallId = callId
479
+ Log.d(TAG, "Call $callId set to ACTIVE state")
551
480
 
552
- // Stop media AFTER getting audio focus
481
+ // Clean up media and UI
553
482
  stopRingtone()
554
483
  stopRingback()
555
484
  cancelIncomingCallUI()
556
485
 
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
- }
486
+ // Request audio focus (but don't fail if not granted)
487
+ requestAudioFocus()
566
488
 
567
489
  if (!canMakeMultipleCalls) {
568
490
  activeCalls.filter { it.key != callId }.values.forEach { otherCall ->
@@ -575,20 +497,17 @@ object CallEngine {
575
497
  bringAppToForeground()
576
498
  startForegroundService()
577
499
  keepScreenAwake(true)
578
- resetAudioMode()
500
+ setAudioMode()
579
501
  updateLockScreenBypass()
580
- updateForegroundNotification()
581
502
 
582
- // FIXED: Emit different events based on call direction
503
+ // Always emit events based on call direction
583
504
  if (isLocalAnswer) {
584
- // This is for incoming calls - user answered locally
585
505
  emitCallAnsweredWithMetadata(callId)
586
506
  } else {
587
- // This is for outgoing calls - remote party answered
588
507
  emitOutgoingCallAnsweredWithMetadata(callId)
589
508
  }
590
509
 
591
- Log.d(TAG, "Call $callId successfully answered and UI cleaned up")
510
+ Log.d(TAG, "Call $callId successfully answered")
592
511
  }
593
512
 
594
513
  // For incoming calls (local answer)
@@ -605,8 +524,7 @@ object CallEngine {
605
524
  try {
606
525
  put("metadata", JSONObject(it))
607
526
  } catch (e: Exception) {
608
- Log.w(TAG, "Invalid metadata JSON for callId: $callId", e)
609
- put("metadata", it) // fallback to string
527
+ put("metadata", it)
610
528
  }
611
529
  }
612
530
  })
@@ -626,8 +544,7 @@ object CallEngine {
626
544
  try {
627
545
  put("metadata", JSONObject(it))
628
546
  } catch (e: Exception) {
629
- Log.w(TAG, "Invalid metadata JSON for callId: $callId", e)
630
- put("metadata", it) // fallback to string
547
+ put("metadata", it)
631
548
  }
632
549
  }
633
550
  })
@@ -640,21 +557,16 @@ object CallEngine {
640
557
 
641
558
  fun setOnHold(callId: String, onHold: Boolean) {
642
559
  Log.d(TAG, "setOnHold: $callId, onHold: $onHold")
643
-
644
560
  val callInfo = activeCalls[callId]
645
561
  if (callInfo == null) {
646
- Log.w(TAG, "Cannot set hold state for call $callId - not found in active calls")
562
+ Log.w(TAG, "Cannot set hold state for call $callId - not found")
647
563
  return
648
564
  }
649
565
 
650
- if (onHold) {
651
- if (callInfo.state == CallState.ACTIVE) {
652
- holdCallInternal(callId, heldBySystem = false)
653
- }
654
- } else {
655
- if (callInfo.state == CallState.HELD) {
656
- unholdCallInternal(callId, resumedBySystem = false)
657
- }
566
+ if (onHold && callInfo.state == CallState.ACTIVE) {
567
+ holdCallInternal(callId, heldBySystem = false)
568
+ } else if (!onHold && callInfo.state == CallState.HELD) {
569
+ unholdCallInternal(callId, resumedBySystem = false)
658
570
  }
659
571
  }
660
572
 
@@ -662,7 +574,7 @@ object CallEngine {
662
574
  Log.d(TAG, "holdCallInternal: $callId, heldBySystem: $heldBySystem")
663
575
  val callInfo = activeCalls[callId]
664
576
  if (callInfo?.state != CallState.ACTIVE) {
665
- Log.w(TAG, "Cannot hold call $callId - not in active state (current: ${callInfo?.state})")
577
+ Log.w(TAG, "Cannot hold call $callId - not in active state")
666
578
  return
667
579
  }
668
580
 
@@ -671,9 +583,7 @@ object CallEngine {
671
583
  wasHeldBySystem = heldBySystem
672
584
  )
673
585
 
674
- val connection = telecomConnections[callId]
675
- connection?.setOnHold()
676
-
586
+ telecomConnections[callId]?.setOnHold()
677
587
  updateForegroundNotification()
678
588
  emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
679
589
  updateLockScreenBypass()
@@ -687,29 +597,19 @@ object CallEngine {
687
597
  Log.d(TAG, "unholdCallInternal: $callId, resumedBySystem: $resumedBySystem")
688
598
  val callInfo = activeCalls[callId]
689
599
  if (callInfo?.state != CallState.HELD) {
690
- Log.w(TAG, "Cannot unhold call $callId - not in held state (current: ${callInfo?.state})")
600
+ Log.w(TAG, "Cannot unhold call $callId - not in held state")
691
601
  return
692
602
  }
693
603
 
694
- // FIXED: Simplified audio focus check to prevent UNHELD FAILED
695
- if (!hasAudioFocus && !resumedBySystem && !requestAudioFocus()) {
696
- Log.w(TAG, "Failed to get audio focus for unhold - but continuing anyway")
697
- // Don't emit UNHELD FAILED - just continue
698
- }
699
-
700
604
  activeCalls[callId] = callInfo.copy(
701
605
  state = CallState.ACTIVE,
702
606
  wasHeldBySystem = false
703
607
  )
704
608
 
705
- val connection = telecomConnections[callId]
706
- connection?.setActive()
707
-
609
+ telecomConnections[callId]?.setActive()
708
610
  updateForegroundNotification()
709
611
  emitEvent(CallEventType.CALL_UNHELD, JSONObject().put("callId", callId))
710
612
  updateLockScreenBypass()
711
-
712
- Log.d(TAG, "Call $callId successfully unheld")
713
613
  }
714
614
 
715
615
  fun muteCall(callId: String) {
@@ -721,18 +621,17 @@ object CallEngine {
721
621
  }
722
622
 
723
623
  fun setMuted(callId: String, muted: Boolean) {
724
- Log.d(TAG, "setMuted: $callId, muted: $muted")
725
624
  setMutedInternal(callId, muted)
726
625
  }
727
626
 
728
627
  private fun setMutedInternal(callId: String, muted: Boolean) {
729
- val context = requireContext()
730
628
  val callInfo = activeCalls[callId]
731
629
  if (callInfo == null) {
732
- Log.w(TAG, "Cannot set mute state for call $callId - not found in active calls")
630
+ Log.w(TAG, "Cannot set mute state for call $callId - not found")
733
631
  return
734
632
  }
735
633
 
634
+ val context = requireContext()
736
635
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
737
636
 
738
637
  val wasMuted = audioManager?.isMicrophoneMute ?: false
@@ -752,11 +651,8 @@ object CallEngine {
752
651
  }
753
652
 
754
653
  fun endAllCalls() {
755
- Log.d(TAG, "endAllCalls: Ending all active calls.")
756
- if (activeCalls.isEmpty()) {
757
- Log.d(TAG, "No active calls, nothing to do.")
758
- return
759
- }
654
+ Log.d(TAG, "endAllCalls: Ending all active calls")
655
+ if (activeCalls.isEmpty()) return
760
656
 
761
657
  activeCalls.keys.toList().forEach { callId ->
762
658
  endCallInternal(callId)
@@ -767,7 +663,7 @@ object CallEngine {
767
663
  callMetadata.clear()
768
664
  currentCallId = null
769
665
 
770
- finalCleanup()
666
+ cleanup()
771
667
  updateLockScreenBypass()
772
668
  }
773
669
 
@@ -779,12 +675,10 @@ object CallEngine {
779
675
  return
780
676
  }
781
677
 
782
- // Get metadata before removing
783
678
  val metadata = callMetadata.remove(callId)
784
679
 
785
680
  activeCalls[callId] = callInfo.copy(state = CallState.ENDED)
786
681
  activeCalls.remove(callId)
787
- Log.d(TAG, "Call $callId removed from activeCalls. Remaining: ${activeCalls.size}")
788
682
 
789
683
  stopRingback()
790
684
  stopRingtone()
@@ -792,7 +686,6 @@ object CallEngine {
792
686
 
793
687
  if (currentCallId == callId) {
794
688
  currentCallId = activeCalls.filter { it.value.state != CallState.ENDED }.keys.firstOrNull()
795
- Log.d(TAG, "Current call was $callId. New currentCallId: $currentCallId")
796
689
  }
797
690
 
798
691
  val connection = telecomConnections[callId]
@@ -800,11 +693,10 @@ object CallEngine {
800
693
  connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
801
694
  connection.destroy()
802
695
  removeTelecomConnection(callId)
803
- Log.d(TAG, "Telecom Connection for $callId disconnected and destroyed.")
804
696
  }
805
697
 
806
698
  if (activeCalls.isEmpty()) {
807
- finalCleanup()
699
+ cleanup()
808
700
  } else {
809
701
  updateForegroundNotification()
810
702
  }
@@ -818,8 +710,7 @@ object CallEngine {
818
710
  try {
819
711
  put("metadata", JSONObject(it))
820
712
  } catch (e: Exception) {
821
- Log.w(TAG, "Invalid metadata JSON for callId: $callId", e)
822
- put("metadata", it) // fallback to string
713
+ put("metadata", it)
823
714
  }
824
715
  }
825
716
  })
@@ -829,7 +720,6 @@ object CallEngine {
829
720
  fun getAudioDevices(): AudioRoutesInfo {
830
721
  val context = requireContext()
831
722
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
832
- Log.e(TAG, "getAudioDevices: AudioManager is null. Returning default.")
833
723
  return AudioRoutesInfo(emptyArray(), "Unknown")
834
724
  }
835
725
 
@@ -865,15 +755,13 @@ object CallEngine {
865
755
  else -> "Earpiece"
866
756
  }
867
757
 
868
- val result = AudioRoutesInfo(devices.toTypedArray(), currentRoute)
869
- Log.d(TAG, "Audio devices info: $result")
870
- return result
758
+ return AudioRoutesInfo(devices.toTypedArray(), currentRoute)
871
759
  }
872
760
 
873
761
  fun setAudioRoute(route: String) {
874
762
  val context = requireContext()
875
763
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
876
- Log.d(TAG, "Attempting to set audio route to: $route. Current mode: ${audioManager?.mode}")
764
+ Log.d(TAG, "Setting audio route to: $route")
877
765
 
878
766
  val previousRoute = getCurrentAudioRoute()
879
767
 
@@ -883,26 +771,22 @@ object CallEngine {
883
771
 
884
772
  when (route) {
885
773
  "Speaker" -> {
886
- Log.d(TAG, "Setting audio route to Speaker.")
887
774
  audioManager?.isSpeakerphoneOn = true
888
775
  audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
889
776
  }
890
777
  "Earpiece" -> {
891
- Log.d(TAG, "Setting audio route to Earpiece.")
892
778
  audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
893
779
  }
894
780
  "Bluetooth" -> {
895
- Log.d(TAG, "Setting audio route to Bluetooth.")
896
781
  audioManager?.startBluetoothSco()
897
782
  audioManager?.isBluetoothScoOn = true
898
783
  audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
899
784
  }
900
785
  "Headset" -> {
901
- Log.d(TAG, "Setting audio route to Headset (wired).")
902
786
  audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
903
787
  }
904
788
  else -> {
905
- Log.w(TAG, "Unknown audio route: $route. No action taken.")
789
+ Log.w(TAG, "Unknown audio route: $route")
906
790
  return
907
791
  }
908
792
  }
@@ -932,34 +816,31 @@ object CallEngine {
932
816
  else -> "Earpiece"
933
817
  }
934
818
 
935
- Log.d(TAG, "Setting initial audio route for $callType call: $defaultRoute")
819
+ Log.d(TAG, "Setting initial audio route: $defaultRoute")
936
820
  setAudioRoute(defaultRoute)
937
821
  }
938
822
 
823
+ private fun setAudioMode() {
824
+ audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
825
+ }
826
+
939
827
  private fun resetAudioMode() {
940
- val context = requireContext()
941
- audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
942
828
  if (activeCalls.isEmpty()) {
943
- Log.d(TAG, "Resetting audio mode to NORMAL as no active calls remain.")
944
829
  audioManager?.mode = AudioManager.MODE_NORMAL
945
830
  audioManager?.stopBluetoothSco()
946
831
  audioManager?.isBluetoothScoOn = false
947
832
  audioManager?.isSpeakerphoneOn = false
948
833
  abandonAudioFocus()
949
- } else {
950
- Log.d(TAG, "Audio mode not reset; ${activeCalls.size} calls still active.")
951
834
  }
952
835
  }
953
836
 
954
837
  // --- Audio Device Callback ---
955
838
  private val audioDeviceCallback = object : AudioDeviceCallback() {
956
839
  override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
957
- Log.d(TAG, "Audio devices added. Checking for changes.")
958
840
  emitAudioDevicesChangedIfNeeded()
959
841
  }
960
842
 
961
843
  override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
962
- Log.d(TAG, "Audio devices removed. Checking for changes.")
963
844
  emitAudioDevicesChangedIfNeeded()
964
845
  }
965
846
  }
@@ -968,14 +849,12 @@ object CallEngine {
968
849
  val context = requireContext()
969
850
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
970
851
  audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
971
- Log.d(TAG, "Audio device callback registered.")
972
852
  }
973
853
 
974
854
  fun unregisterAudioDeviceCallback() {
975
855
  val context = requireContext()
976
856
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
977
857
  audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
978
- Log.d(TAG, "Audio device callback unregistered.")
979
858
  }
980
859
 
981
860
  private fun emitAudioDevicesChangedIfNeeded() {
@@ -1004,14 +883,14 @@ object CallEngine {
1004
883
  PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
1005
884
  "CallEngine:WakeLock"
1006
885
  )
1007
- wakeLock?.acquire(10 * 60 * 1000L /* 10 minutes */)
1008
- Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK.")
886
+ wakeLock?.acquire(10 * 60 * 1000L)
887
+ Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK")
1009
888
  }
1010
889
  } else {
1011
890
  wakeLock?.let {
1012
891
  if (it.isHeld) {
1013
892
  it.release()
1014
- Log.d(TAG, "Released SCREEN_DIM_WAKE_LOCK.")
893
+ Log.d(TAG, "Released SCREEN_DIM_WAKE_LOCK")
1015
894
  }
1016
895
  }
1017
896
  wakeLock = null
@@ -1035,17 +914,7 @@ object CallEngine {
1035
914
  }
1036
915
 
1037
916
  private fun rejectIncomingCallCollision(callId: String, reason: String) {
1038
- // Remove metadata for rejected call
1039
917
  callMetadata.remove(callId)
1040
-
1041
- CoroutineScope(Dispatchers.IO).launch {
1042
- try {
1043
- Log.d(TAG, "Server rejection request would be made here for callId: $callId, reason: $reason")
1044
- } catch (e: Exception) {
1045
- Log.e(TAG, "Failed to send rejection to server", e)
1046
- }
1047
- }
1048
-
1049
918
  emitEvent(CallEventType.CALL_REJECTED, JSONObject().apply {
1050
919
  put("callId", callId)
1051
920
  put("reason", reason)
@@ -1081,13 +950,12 @@ object CallEngine {
1081
950
 
1082
951
  val manager = context.getSystemService(NotificationManager::class.java)
1083
952
  manager.createNotificationChannel(channel)
1084
- Log.d(TAG, "Notification channel '$NOTIF_CHANNEL_ID' created/updated.")
1085
953
  }
1086
954
  }
1087
955
 
1088
956
  private fun showIncomingCallUI(callId: String, callerName: String, callType: String) {
1089
957
  val context = requireContext()
1090
- Log.d(TAG, "Showing incoming call UI for $callId, caller: $callerName, callType: $callType")
958
+ Log.d(TAG, "Showing incoming call UI for $callId")
1091
959
  createNotificationChannel()
1092
960
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
1093
961
 
@@ -1153,33 +1021,27 @@ object CallEngine {
1153
1021
  setInitialAudioRoute(callType)
1154
1022
  }
1155
1023
 
1156
- // Made public for CallActivity
1157
1024
  fun cancelIncomingCallUI() {
1158
1025
  val context = requireContext()
1159
- Log.d(TAG, "Cancelling incoming call UI.")
1160
1026
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
1161
1027
  notificationManager.cancel(NOTIF_ID)
1162
1028
  stopRingtone()
1163
1029
  }
1164
1030
 
1165
- // --- Service Management ---
1031
+ // --- Service Management - SIMPLIFIED ---
1166
1032
  private fun startForegroundService() {
1167
1033
  val context = requireContext()
1168
- Log.d(TAG, "Starting CallForegroundService.")
1169
-
1170
1034
  val currentCall = activeCalls.values.find {
1171
1035
  it.state == CallState.ACTIVE || it.state == CallState.INCOMING ||
1172
1036
  it.state == CallState.DIALING || it.state == CallState.HELD
1173
1037
  }
1174
1038
 
1175
1039
  val intent = Intent(context, CallForegroundService::class.java)
1176
-
1177
1040
  if (currentCall != null) {
1178
1041
  intent.putExtra("callId", currentCall.callId)
1179
1042
  intent.putExtra("callType", currentCall.callType)
1180
1043
  intent.putExtra("displayName", currentCall.displayName)
1181
1044
  intent.putExtra("state", currentCall.state.name)
1182
- Log.d(TAG, "Starting foreground service with call info: ${currentCall.callId}")
1183
1045
  }
1184
1046
 
1185
1047
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -1191,11 +1053,14 @@ object CallEngine {
1191
1053
 
1192
1054
  private fun stopForegroundService() {
1193
1055
  val context = requireContext()
1194
- Log.d(TAG, "Stopping CallForegroundService.")
1195
1056
  val intent = Intent(context, CallForegroundService::class.java)
1196
1057
  context.stopService(intent)
1197
1058
  }
1198
1059
 
1060
+ private fun updateForegroundNotification() {
1061
+ startForegroundService() // Just restart the service with updated info
1062
+ }
1063
+
1199
1064
  private fun bringAppToForeground() {
1200
1065
  val context = requireContext()
1201
1066
  val packageName = context.packageName
@@ -1204,11 +1069,6 @@ object CallEngine {
1204
1069
 
1205
1070
  if (isCallActive()) {
1206
1071
  launchIntent?.putExtra("BYPASS_LOCK_SCREEN", true)
1207
- launchIntent?.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
1208
- Log.d(TAG, "App brought to foreground with lock screen bypass request for active call")
1209
- } else {
1210
- launchIntent?.removeExtra("BYPASS_LOCK_SCREEN")
1211
- Log.d(TAG, "App brought to foreground without lock screen bypass")
1212
1072
  }
1213
1073
 
1214
1074
  try {
@@ -1234,14 +1094,10 @@ object CallEngine {
1234
1094
 
1235
1095
  try {
1236
1096
  telecomManager.registerPhoneAccount(phoneAccount)
1237
- Log.d(TAG, "PhoneAccount registered successfully.")
1238
- } catch (e: SecurityException) {
1239
- Log.e(TAG, "SecurityException: Cannot register PhoneAccount. Missing MANAGE_OWN_CALLS permission?", e)
1097
+ Log.d(TAG, "PhoneAccount registered successfully")
1240
1098
  } catch (e: Exception) {
1241
1099
  Log.e(TAG, "Failed to register PhoneAccount: ${e.message}", e)
1242
1100
  }
1243
- } else {
1244
- Log.d(TAG, "PhoneAccount already registered.")
1245
1101
  }
1246
1102
  }
1247
1103
 
@@ -1257,115 +1113,65 @@ object CallEngine {
1257
1113
  private fun playRingtone() {
1258
1114
  val context = requireContext()
1259
1115
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1260
- Log.d(TAG, "playRingtone: Android S+ detected, system will handle ringtone via Telecom.")
1261
- return
1116
+ return // System handles it
1262
1117
  }
1263
1118
 
1264
1119
  try {
1265
- Log.d(TAG, "Playing ringtone (for Android < S).")
1266
1120
  val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
1267
1121
  ringtone = RingtoneManager.getRingtone(context, uri)
1268
- ringtone?.audioAttributes = AudioAttributes.Builder()
1269
- .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
1270
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
1271
- .build()
1272
1122
  ringtone?.play()
1273
1123
  } catch (e: Exception) {
1274
- Log.e(TAG, "Failed to play ringtone: ${e.message}", e)
1124
+ Log.e(TAG, "Failed to play ringtone: ${e.message}")
1275
1125
  }
1276
1126
  }
1277
1127
 
1278
- // Made public for CallActivity and CallManager
1279
1128
  fun stopRingtone() {
1280
1129
  try {
1281
- if (ringtone?.isPlaying == true) {
1282
- ringtone?.stop()
1283
- Log.d(TAG, "Ringtone stopped.")
1284
- }
1130
+ ringtone?.stop()
1285
1131
  } catch (e: Exception) {
1286
- Log.e(TAG, "Error stopping ringtone: ${e.message}", e)
1132
+ Log.e(TAG, "Error stopping ringtone: ${e.message}")
1287
1133
  }
1288
1134
  ringtone = null
1289
1135
  }
1290
1136
 
1291
1137
  private fun startRingback() {
1292
1138
  val context = requireContext()
1293
- if (ringbackPlayer?.isPlaying == true) {
1294
- Log.d(TAG, "Ringback tone already playing.")
1295
- return
1296
- }
1139
+ if (ringbackPlayer?.isPlaying == true) return
1297
1140
 
1298
1141
  try {
1299
1142
  val ringbackUri = Uri.parse("android.resource://${context.packageName}/raw/ringback_tone")
1300
1143
  ringbackPlayer = MediaPlayer.create(context, ringbackUri)
1301
- if (ringbackPlayer == null) {
1302
- Log.e(TAG, "Failed to create MediaPlayer for ringback. Check raw/ringback_tone.mp3 exists.")
1303
- return
1304
- }
1305
-
1306
1144
  ringbackPlayer?.apply {
1307
1145
  isLooping = true
1308
- setAudioAttributes(
1309
- AudioAttributes.Builder()
1310
- .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
1311
- .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
1312
- .build()
1313
- )
1314
1146
  start()
1315
- Log.d(TAG, "Ringback tone started.")
1316
1147
  }
1317
1148
  } catch (e: Exception) {
1318
- Log.e(TAG, "Failed to play ringback tone: ${e.message}", e)
1149
+ Log.e(TAG, "Failed to play ringback tone: ${e.message}")
1319
1150
  }
1320
1151
  }
1321
1152
 
1322
1153
  private fun stopRingback() {
1323
1154
  try {
1324
- if (ringbackPlayer?.isPlaying == true) {
1325
- ringbackPlayer?.stop()
1326
- ringbackPlayer?.release()
1327
- Log.d(TAG, "Ringback tone stopped and released.")
1328
- }
1155
+ ringbackPlayer?.stop()
1156
+ ringbackPlayer?.release()
1329
1157
  } catch (e: Exception) {
1330
- Log.e(TAG, "Error stopping ringback tone: ${e.message}", e)
1158
+ Log.e(TAG, "Error stopping ringback: ${e.message}")
1331
1159
  } finally {
1332
1160
  ringbackPlayer = null
1333
1161
  }
1334
1162
  }
1335
1163
 
1336
- private fun updateForegroundNotification() {
1337
- val context = requireContext()
1338
- val activeCall = activeCalls.values.find { it.state == CallState.ACTIVE }
1339
- val heldCall = activeCalls.values.find { it.state == CallState.HELD }
1340
-
1341
- val callToShow = activeCall ?: heldCall
1342
- callToShow?.let {
1343
- val intent = Intent(context, CallForegroundService::class.java)
1344
- intent.putExtra("UPDATE_NOTIFICATION", true)
1345
- intent.putExtra("callId", it.callId)
1346
- intent.putExtra("callType", it.callType)
1347
- intent.putExtra("displayName", it.displayName)
1348
- intent.putExtra("state", it.state.name)
1349
-
1350
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1351
- context.startForegroundService(intent)
1352
- } else {
1353
- context.startService(intent)
1354
- }
1355
- }
1356
- }
1357
-
1358
- private fun finalCleanup() {
1359
- Log.d(TAG, "Performing final cleanup - no active calls remaining")
1164
+ // --- Cleanup - SIMPLIFIED ---
1165
+ private fun cleanup() {
1166
+ Log.d(TAG, "Performing cleanup")
1360
1167
  stopForegroundService()
1361
1168
  keepScreenAwake(false)
1362
1169
  resetAudioMode()
1363
- isSystemCallActive = false
1364
1170
  }
1365
1171
 
1366
1172
  // --- Lifecycle Management ---
1367
1173
  fun onApplicationTerminate() {
1368
- Log.d(TAG, "Application terminating - cleaning up all calls")
1174
+ Log.d(TAG, "Application terminating")
1369
1175
 
1370
1176
  // End all calls properly
1371
1177
  activeCalls.keys.toList().forEach { callId ->
@@ -1380,8 +1186,7 @@ object CallEngine {
1380
1186
  callMetadata.clear()
1381
1187
  currentCallId = null
1382
1188
 
1383
- // Release resources
1384
- finalCleanup()
1189
+ cleanup()
1385
1190
 
1386
1191
  // Clear callbacks
1387
1192
  lockScreenBypassCallbacks.clear()
@@ -35,10 +35,8 @@ class CallForegroundService : Service() {
35
35
  val state = intent?.getStringExtra("state")
36
36
 
37
37
  val notification = if (callId != null && callType != null && displayName != null && state != null) {
38
- Log.d(TAG, "Building enhanced notification with call info: $callId")
39
38
  buildEnhancedNotification(callId, callType, displayName, state)
40
39
  } else {
41
- Log.d(TAG, "Building basic notification - no call info available")
42
40
  buildBasicNotification()
43
41
  }
44
42
 
@@ -46,14 +44,9 @@ class CallForegroundService : Service() {
46
44
  return START_STICKY
47
45
  }
48
46
 
49
- override fun onBind(intent: Intent?): IBinder? {
50
- Log.d(TAG, "Service onBind")
51
- return null
52
- }
47
+ override fun onBind(intent: Intent?): IBinder? = null
53
48
 
54
49
  private fun buildBasicNotification(): Notification {
55
- Log.d(TAG, "Building basic foreground notification.")
56
-
57
50
  return NotificationCompat.Builder(this, CHANNEL_ID)
58
51
  .setContentTitle("Call Service")
59
52
  .setContentText("Call service is running...")
@@ -61,12 +54,11 @@ class CallForegroundService : Service() {
61
54
  .setOngoing(true)
62
55
  .setCategory(NotificationCompat.CATEGORY_CALL)
63
56
  .setPriority(NotificationCompat.PRIORITY_DEFAULT)
64
- .setWhen(System.currentTimeMillis())
65
57
  .build()
66
58
  }
67
59
 
68
60
  private fun buildEnhancedNotification(callId: String, callType: String, displayName: String, state: String): Notification {
69
- Log.d(TAG, "Building enhanced foreground notification for callId: $callId, state: $state")
61
+ Log.d(TAG, "Building notification for callId: $callId, state: $state")
70
62
 
71
63
  val endCallIntent = Intent(this, CallNotificationActionReceiver::class.java).apply {
72
64
  action = "com.qusaieilouti99.callmanager.END_CALL"
@@ -90,16 +82,6 @@ class CallForegroundService : Service() {
90
82
  PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
91
83
  )
92
84
 
93
- val mainIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
94
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
95
- }
96
- val mainPendingIntent = mainIntent?.let {
97
- PendingIntent.getActivity(
98
- this, 102, it,
99
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
100
- )
101
- }
102
-
103
85
  val statusText = when (state) {
104
86
  "ACTIVE" -> displayName
105
87
  "HELD" -> "$displayName (on hold)"
@@ -123,8 +105,8 @@ class CallForegroundService : Service() {
123
105
  .setOngoing(true)
124
106
  .setCategory(NotificationCompat.CATEGORY_CALL)
125
107
  .setPriority(NotificationCompat.PRIORITY_HIGH)
126
- .setWhen(System.currentTimeMillis())
127
108
 
109
+ // Add action buttons for ACTIVE and HELD calls
128
110
  if (state == "ACTIVE" || state == "HELD") {
129
111
  notificationBuilder
130
112
  .addAction(
@@ -145,10 +127,6 @@ class CallForegroundService : Service() {
145
127
  )
146
128
  }
147
129
 
148
- mainPendingIntent?.let {
149
- notificationBuilder.setContentIntent(it)
150
- }
151
-
152
130
  return notificationBuilder.build()
153
131
  }
154
132
 
@@ -166,37 +144,28 @@ class CallForegroundService : Service() {
166
144
 
167
145
  val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
168
146
  manager.createNotificationChannel(channel)
169
- Log.d(TAG, "Foreground notification channel '$CHANNEL_ID' created/updated.")
170
147
  }
171
148
  }
172
149
 
173
150
  override fun onTaskRemoved(rootIntent: Intent?) {
174
- // Figure out which Activity’s task was just removed:
175
151
  val removed = rootIntent?.component?.className
176
- Log.d(TAG, "onTaskRemoved: removedActivity=$removed")
152
+ Log.d(TAG, "onTaskRemoved: $removed")
177
153
 
178
- // If it was our lock-screen CallActivity, ignore
179
- // we only want to clean up when the MAIN app task is swiped away.
180
- if (removed == CallActivity::class.java.name) {
181
- Log.d(TAG, "CallActivity was removed; keeping call alive.")
182
- return
154
+ // Only terminate if main app was removed, not CallActivity
155
+ if (removed != CallActivity::class.java.name) {
156
+ Log.d(TAG, "Main app task removed - terminating")
157
+ CallEngine.onApplicationTerminate()
183
158
  }
184
159
 
185
- // Otherwise (e.g. MainActivity removed), tear everything down:
186
- Log.d(TAG, "Main task removed; ending all calls.")
187
- CallEngine.onApplicationTerminate()
188
- stopSelf()
189
160
  super.onTaskRemoved(rootIntent)
190
- }
161
+ }
191
162
 
192
163
  override fun onDestroy() {
193
164
  super.onDestroy()
194
- Log.d(TAG, "Service onDestroy. Stopping foreground.")
165
+ Log.d(TAG, "Service onDestroy")
195
166
  stopForeground(true)
196
167
 
197
- // Additional cleanup when service is destroyed
198
- if (!CallEngine.isCallActive()) {
199
- CallEngine.onApplicationTerminate()
200
- }
168
+ // SIMPLIFIED: Don't call onApplicationTerminate here
169
+ // Only onTaskRemoved should trigger app termination
201
170
  }
202
171
  }
@@ -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.64",
3
+ "version": "0.1.66",
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