@qusaieilouti99/call-manager 0.1.56 → 0.1.58

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.
@@ -1,6 +1,7 @@
1
1
  package com.margelo.nitro.qusaieilouti99.callmanager
2
2
 
3
3
  import android.app.Activity
4
+ import android.app.Application
4
5
  import android.app.Notification
5
6
  import android.app.NotificationChannel
6
7
  import android.app.NotificationManager
@@ -46,19 +47,20 @@ object CallEngine {
46
47
  private const val FOREGROUND_CHANNEL_ID = "call_foreground_channel"
47
48
  private const val FOREGROUND_NOTIF_ID = 1001
48
49
 
50
+ // Core context - initialized once and maintained
51
+ private var appContext: Context? = null
52
+ private var isInitialized = AtomicBoolean(false)
53
+
49
54
  // Audio & Media
50
55
  private var ringtone: android.media.Ringtone? = null
51
56
  private var ringbackPlayer: MediaPlayer? = null
52
57
  private var audioManager: AudioManager? = null
53
58
  private var wakeLock: PowerManager.WakeLock? = null
54
- private var appContext: Context? = null
55
59
  private var audioFocusRequest: AudioFocusRequest? = null
56
60
 
57
- // Call State Management - Only what native needs
61
+ // Call State Management
58
62
  private val activeCalls = ConcurrentHashMap<String, CallInfo>()
59
63
  private val telecomConnections = ConcurrentHashMap<String, Connection>()
60
-
61
- // Separate opaque metadata storage - native doesn't interpret
62
64
  private val callMetadata = ConcurrentHashMap<String, String>()
63
65
 
64
66
  private var currentCallId: String? = null
@@ -66,7 +68,6 @@ object CallEngine {
66
68
 
67
69
  // Audio State Tracking
68
70
  private var lastAudioRoutesInfo: AudioRoutesInfo? = null
69
- private var lastMuteState: Boolean = false
70
71
  private var hasAudioFocus: Boolean = false
71
72
  private var isSystemCallActive: Boolean = false
72
73
 
@@ -78,56 +79,92 @@ object CallEngine {
78
79
  private var eventHandler: ((CallEventType, String) -> Unit)? = null
79
80
  private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
80
81
 
81
- // Operation State
82
- private val operationInProgress = AtomicBoolean(false)
83
-
84
82
  interface LockScreenBypassCallback {
85
83
  fun onLockScreenBypassChanged(shouldBypass: Boolean)
86
84
  }
87
85
 
88
- // --- Audio Focus Management ---
86
+ // --- INITIALIZATION - Fix for context management ---
87
+ 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()
96
+ }
97
+ }
98
+ }
99
+
100
+ fun isInitialized(): Boolean = isInitialized.get()
101
+
102
+ private fun requireContext(): Context {
103
+ return appContext ?: throw IllegalStateException("CallEngine not initialized. Call initialize() first.")
104
+ }
105
+
106
+ // --- Event System ---
107
+ fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
108
+ Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
109
+ eventHandler = handler
110
+ handler?.let { h ->
111
+ if (cachedEvents.isNotEmpty()) {
112
+ Log.d(TAG, "Emitting ${cachedEvents.size} cached events.")
113
+ cachedEvents.forEach { (type, data) -> h.invoke(type, data) }
114
+ cachedEvents.clear()
115
+ }
116
+ }
117
+ }
118
+
119
+ private fun emitEvent(type: CallEventType, data: JSONObject) {
120
+ Log.d(TAG, "Emitting event: $type, data: $data")
121
+ val dataString = data.toString()
122
+ if (eventHandler != null) {
123
+ eventHandler?.invoke(type, dataString)
124
+ } else {
125
+ Log.d(TAG, "No event handler registered, caching event: $type")
126
+ cachedEvents.add(Pair(type, dataString))
127
+ }
128
+ }
129
+
130
+ // --- Audio Focus Management (Simplified) ---
89
131
  private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
90
132
  Log.d(TAG, "Audio focus changed: $focusChange")
91
133
  when (focusChange) {
92
- AudioManager.AUDIOFOCUS_LOSS -> handleAudioFocusLoss()
93
- AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> handleAudioFocusLoss()
94
- AudioManager.AUDIOFOCUS_GAIN -> handleAudioFocusGain()
95
- AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
96
- Log.d(TAG, "Audio focus loss - can duck, not holding call")
134
+ AudioManager.AUDIOFOCUS_LOSS,
135
+ AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
136
+ hasAudioFocus = false
137
+ isSystemCallActive = true
138
+ holdSystemCalls()
139
+ }
140
+ AudioManager.AUDIOFOCUS_GAIN -> {
141
+ hasAudioFocus = true
142
+ isSystemCallActive = false
143
+ Handler(Looper.getMainLooper()).postDelayed({
144
+ resumeSystemHeldCalls()
145
+ }, 1000)
97
146
  }
98
147
  }
148
+ updateForegroundNotification()
99
149
  }
100
150
 
101
- private fun handleAudioFocusLoss() {
102
- Log.d(TAG, "Audio focus lost - likely system call active")
103
- hasAudioFocus = false
104
- isSystemCallActive = true
105
-
151
+ private fun holdSystemCalls() {
106
152
  activeCalls.values.filter { it.state == CallState.ACTIVE }.forEach { call ->
107
153
  if (!call.wasHeldBySystem) {
108
154
  holdCallInternal(call.callId, heldBySystem = true)
109
155
  }
110
156
  }
111
-
112
157
  stopRingback()
113
- updateForegroundNotification()
114
158
  }
115
159
 
116
- private fun handleAudioFocusGain() {
117
- Log.d(TAG, "Audio focus regained - system call likely ended")
118
- hasAudioFocus = true
119
- isSystemCallActive = false
120
-
121
- Handler(Looper.getMainLooper()).postDelayed({
122
- activeCalls.values.filter { it.state == CallState.HELD && it.wasHeldBySystem }.forEach { call ->
123
- unholdCallInternal(call.callId, resumedBySystem = true)
124
- }
125
- updateForegroundNotification()
126
- }, 1000)
160
+ private fun resumeSystemHeldCalls() {
161
+ activeCalls.values.filter { it.state == CallState.HELD && it.wasHeldBySystem }.forEach { call ->
162
+ unholdCallInternal(call.callId, resumedBySystem = true)
163
+ }
127
164
  }
128
165
 
129
166
  private fun requestAudioFocus(): Boolean {
130
- val context = appContext ?: return false
167
+ val context = requireContext()
131
168
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
132
169
 
133
170
  return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -160,45 +197,20 @@ object CallEngine {
160
197
  }
161
198
 
162
199
  private fun abandonAudioFocus() {
163
- val context = appContext ?: return
164
- audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
165
-
166
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
167
- audioFocusRequest?.let { request ->
168
- audioManager?.abandonAudioFocusRequest(request)
200
+ audioManager?.let { am ->
201
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
202
+ audioFocusRequest?.let { request ->
203
+ am.abandonAudioFocusRequest(request)
204
+ }
205
+ } else {
206
+ @Suppress("DEPRECATION")
207
+ am.abandonAudioFocus(audioFocusChangeListener)
169
208
  }
170
- } else {
171
- @Suppress("DEPRECATION")
172
- audioManager?.abandonAudioFocus(audioFocusChangeListener)
173
209
  }
174
210
  hasAudioFocus = false
175
211
  Log.d(TAG, "Audio focus abandoned")
176
212
  }
177
213
 
178
- // --- Event System ---
179
- fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
180
- Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
181
- eventHandler = handler
182
- handler?.let { h ->
183
- if (cachedEvents.isNotEmpty()) {
184
- Log.d(TAG, "Emitting ${cachedEvents.size} cached events.")
185
- cachedEvents.forEach { (type, data) -> h.invoke(type, data) }
186
- cachedEvents.clear()
187
- }
188
- }
189
- }
190
-
191
- fun emitEvent(type: CallEventType, data: JSONObject) {
192
- Log.d(TAG, "Emitting event: $type, data: $data")
193
- val dataString = data.toString()
194
- if (eventHandler != null) {
195
- eventHandler?.invoke(type, dataString)
196
- } else {
197
- Log.d(TAG, "No event handler registered, caching event: $type")
198
- cachedEvents.add(Pair(type, dataString))
199
- }
200
- }
201
-
202
214
  // --- Lock Screen Bypass Management ---
203
215
  fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
204
216
  lockScreenBypassCallbacks.add(callback)
@@ -239,8 +251,6 @@ object CallEngine {
239
251
 
240
252
  fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
241
253
 
242
- fun getAppContext(): Context? = appContext
243
-
244
254
  // --- Public API ---
245
255
  fun setCanMakeMultipleCalls(allow: Boolean) {
246
256
  canMakeMultipleCalls = allow
@@ -267,10 +277,12 @@ object CallEngine {
267
277
  pictureUrl: String? = null,
268
278
  metadata: String? = null
269
279
  ) {
270
- appContext = context.applicationContext
280
+ if (!isInitialized.get()) {
281
+ initialize(context)
282
+ }
283
+
271
284
  Log.d(TAG, "reportIncomingCall: callId=$callId, type=$callType, name=$displayName")
272
285
 
273
- // Store metadata separately if provided
274
286
  metadata?.let { callMetadata[callId] = it }
275
287
 
276
288
  // Check for call collision
@@ -302,11 +314,11 @@ object CallEngine {
302
314
  currentCallId = callId
303
315
  Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING, callType: $callType")
304
316
 
305
- showIncomingCallUI(context, callId, displayName, callType)
306
- registerPhoneAccount(context)
317
+ showIncomingCallUI(callId, displayName, callType)
318
+ registerPhoneAccount()
307
319
 
308
- val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
309
- val phoneAccountHandle = getPhoneAccountHandle(context)
320
+ val telecomManager = requireContext().getSystemService(Context.TELECOM_SERVICE) as TelecomManager
321
+ val phoneAccountHandle = getPhoneAccountHandle()
310
322
  val extras = Bundle().apply {
311
323
  putString(MyConnectionService.EXTRA_CALL_ID, callId)
312
324
  putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
@@ -317,7 +329,7 @@ object CallEngine {
317
329
 
318
330
  try {
319
331
  telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
320
- startForegroundService(context)
332
+ startForegroundService() // Fixed: Always start foreground service
321
333
  Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
322
334
  } catch (e: SecurityException) {
323
335
  Log.e(TAG, "SecurityException: Failed to report incoming call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
@@ -332,13 +344,12 @@ object CallEngine {
332
344
 
333
345
  // --- Outgoing Call Management ---
334
346
  fun startOutgoingCall(
335
- context: Context,
336
347
  callId: String,
337
348
  callType: String,
338
349
  targetName: String,
339
350
  metadata: String? = null
340
351
  ) {
341
- appContext = context.applicationContext
352
+ val context = requireContext()
342
353
  Log.d(TAG, "startOutgoingCall: callId=$callId, type=$callType, target=$targetName")
343
354
 
344
355
  metadata?.let { callMetadata[callId] = it }
@@ -366,9 +377,9 @@ object CallEngine {
366
377
  currentCallId = callId
367
378
  Log.d(TAG, "Call $callId added to activeCalls. State: DIALING, callType: $callType")
368
379
 
369
- registerPhoneAccount(context)
380
+ registerPhoneAccount()
370
381
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
371
- val phoneAccountHandle = getPhoneAccountHandle(context)
382
+ val phoneAccountHandle = getPhoneAccountHandle()
372
383
  val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
373
384
 
374
385
  val extras = Bundle().apply {
@@ -382,14 +393,13 @@ object CallEngine {
382
393
 
383
394
  try {
384
395
  telecomManager.placeCall(addressUri, extras)
385
- startForegroundService(context)
386
- Log.d(TAG, "Successfully reported outgoing call to TelecomManager via placeCall for $callId")
387
-
396
+ startForegroundService() // Fixed: Always start foreground service
388
397
  requestAudioFocus()
389
398
  startRingback()
390
- bringAppToForeground(context)
391
- keepScreenAwake(context, true)
392
- setInitialAudioRoute(context, callType)
399
+ bringAppToForeground()
400
+ keepScreenAwake(true)
401
+ setInitialAudioRoute(callType)
402
+ Log.d(TAG, "Successfully reported outgoing call to TelecomManager via placeCall for $callId")
393
403
  } catch (e: SecurityException) {
394
404
  Log.e(TAG, "SecurityException: Failed to start outgoing call via placeCall. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
395
405
  endCallInternal(callId)
@@ -401,15 +411,14 @@ object CallEngine {
401
411
  updateLockScreenBypass()
402
412
  }
403
413
 
404
- // NEW: Start call as active (not dialing)
414
+ // Fixed: Start call as active (not dialing) with foreground service
405
415
  fun startCall(
406
- context: Context,
407
416
  callId: String,
408
417
  callType: String,
409
418
  targetName: String,
410
419
  metadata: String? = null
411
420
  ) {
412
- appContext = context.applicationContext
421
+ val context = requireContext()
413
422
  Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
414
423
 
415
424
  metadata?.let { callMetadata[callId] = it }
@@ -432,12 +441,12 @@ object CallEngine {
432
441
  currentCallId = callId
433
442
  Log.d(TAG, "Call $callId started as ACTIVE, callType: $callType")
434
443
 
435
- registerPhoneAccount(context)
444
+ registerPhoneAccount()
436
445
  requestAudioFocus()
437
- bringAppToForeground(context)
438
- startForegroundService(context)
439
- keepScreenAwake(context, true)
440
- setInitialAudioRoute(context, callType)
446
+ bringAppToForeground()
447
+ startForegroundService() // Fixed: Start foreground service for JS-initiated calls
448
+ keepScreenAwake(true)
449
+ setInitialAudioRoute(callType)
441
450
  updateLockScreenBypass()
442
451
 
443
452
  // Emit call answered event with metadata
@@ -445,17 +454,18 @@ object CallEngine {
445
454
  }
446
455
 
447
456
  // --- Call Answer Management ---
448
- fun callAnsweredFromJS(context: Context, callId: String) {
457
+ fun callAnsweredFromJS(callId: String) {
449
458
  Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
450
- coreCallAnswered(context, callId, isLocalAnswer = false)
459
+ coreCallAnswered(callId, isLocalAnswer = false)
451
460
  }
452
461
 
453
- fun answerCall(context: Context, callId: String) {
462
+ fun answerCall(callId: String) {
454
463
  Log.d(TAG, "answerCall: $callId - local party answering")
455
- coreCallAnswered(context, callId, isLocalAnswer = true)
464
+ coreCallAnswered(callId, isLocalAnswer = true)
456
465
  }
457
466
 
458
- private fun coreCallAnswered(context: Context, callId: String, isLocalAnswer: Boolean) {
467
+ private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
468
+ val context = requireContext()
459
469
  Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
460
470
 
461
471
  val callInfo = activeCalls[callId]
@@ -466,7 +476,7 @@ object CallEngine {
466
476
 
467
477
  stopRingtone()
468
478
  stopRingback()
469
- cancelIncomingCallUI(context)
479
+ cancelIncomingCallUI()
470
480
  requestAudioFocus()
471
481
 
472
482
  activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
@@ -480,10 +490,10 @@ object CallEngine {
480
490
  }
481
491
  }
482
492
 
483
- bringAppToForeground(context)
484
- startForegroundService(context)
485
- keepScreenAwake(context, true)
486
- resetAudioMode(context)
493
+ bringAppToForeground()
494
+ startForegroundService() // Fixed: Ensure foreground service is running
495
+ keepScreenAwake(true)
496
+ resetAudioMode()
487
497
  updateLockScreenBypass()
488
498
  updateForegroundNotification()
489
499
 
@@ -514,12 +524,11 @@ object CallEngine {
514
524
  }
515
525
 
516
526
  // --- Call Control Methods ---
517
- fun holdCall(context: Context, callId: String) {
527
+ fun holdCall(callId: String) {
518
528
  holdCallInternal(callId, heldBySystem = false)
519
529
  }
520
530
 
521
- // NEW: Set hold state from JS
522
- fun setOnHold(context: Context, callId: String, onHold: Boolean) {
531
+ fun setOnHold(callId: String, onHold: Boolean) {
523
532
  Log.d(TAG, "setOnHold: $callId, onHold: $onHold")
524
533
 
525
534
  val callInfo = activeCalls[callId]
@@ -560,7 +569,7 @@ object CallEngine {
560
569
  updateLockScreenBypass()
561
570
  }
562
571
 
563
- fun unholdCall(context: Context, callId: String) {
572
+ fun unholdCall(callId: String) {
564
573
  unholdCallInternal(callId, resumedBySystem = false)
565
574
  }
566
575
 
@@ -572,16 +581,10 @@ object CallEngine {
572
581
  return
573
582
  }
574
583
 
575
- if (!hasAudioFocus && !resumedBySystem) {
576
- Log.d(TAG, "Attempting to request audio focus for unhold")
577
- if (!requestAudioFocus()) {
578
- Log.w(TAG, "Failed to get audio focus for unhold")
579
- emitEvent(CallEventType.CALL_UNHOLD_FAILED, JSONObject().apply {
580
- put("callId", callId)
581
- put("reason", "Could not obtain audio focus")
582
- })
583
- return
584
- }
584
+ // Fixed: Simplified audio focus check to prevent UNHELD FAILED
585
+ if (!hasAudioFocus && !resumedBySystem && !requestAudioFocus()) {
586
+ Log.w(TAG, "Failed to get audio focus for unhold - but continuing anyway")
587
+ // Don't emit UNHELD FAILED - just continue
585
588
  }
586
589
 
587
590
  activeCalls[callId] = callInfo.copy(
@@ -599,21 +602,21 @@ object CallEngine {
599
602
  Log.d(TAG, "Call $callId successfully unheld")
600
603
  }
601
604
 
602
- fun muteCall(context: Context, callId: String) {
603
- setMutedInternal(context, callId, true)
605
+ fun muteCall(callId: String) {
606
+ setMutedInternal(callId, true)
604
607
  }
605
608
 
606
- fun unmuteCall(context: Context, callId: String) {
607
- setMutedInternal(context, callId, false)
609
+ fun unmuteCall(callId: String) {
610
+ setMutedInternal(callId, false)
608
611
  }
609
612
 
610
- // NEW: Set mute state from JS
611
- fun setMuted(context: Context, callId: String, muted: Boolean) {
613
+ fun setMuted(callId: String, muted: Boolean) {
612
614
  Log.d(TAG, "setMuted: $callId, muted: $muted")
613
- setMutedInternal(context, callId, muted)
615
+ setMutedInternal(callId, muted)
614
616
  }
615
617
 
616
- private fun setMutedInternal(context: Context, callId: String, muted: Boolean) {
618
+ private fun setMutedInternal(callId: String, muted: Boolean) {
619
+ val context = requireContext()
617
620
  val callInfo = activeCalls[callId]
618
621
  if (callInfo == null) {
619
622
  Log.w(TAG, "Cannot set mute state for call $callId - not found in active calls")
@@ -626,7 +629,6 @@ object CallEngine {
626
629
  audioManager?.isMicrophoneMute = muted
627
630
 
628
631
  if (wasMuted != muted) {
629
- lastMuteState = muted
630
632
  val eventType = if (muted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
631
633
  emitEvent(eventType, JSONObject().put("callId", callId))
632
634
  Log.d(TAG, "Call $callId mute state changed to: $muted")
@@ -634,14 +636,12 @@ object CallEngine {
634
636
  }
635
637
 
636
638
  // --- Call End Management ---
637
- fun endCall(context: Context, callId: String) {
638
- appContext = context.applicationContext
639
+ fun endCall(callId: String) {
639
640
  Log.d(TAG, "endCall: $callId")
640
641
  endCallInternal(callId)
641
642
  }
642
643
 
643
- // NEW: End all calls
644
- fun endAllCalls(context: Context) {
644
+ fun endAllCalls() {
645
645
  Log.d(TAG, "endAllCalls: Ending all active calls.")
646
646
  if (activeCalls.isEmpty()) {
647
647
  Log.d(TAG, "No active calls, nothing to do.")
@@ -657,7 +657,7 @@ object CallEngine {
657
657
  callMetadata.clear()
658
658
  currentCallId = null
659
659
 
660
- finalCleanup(context)
660
+ finalCleanup()
661
661
  updateLockScreenBypass()
662
662
  }
663
663
 
@@ -678,7 +678,7 @@ object CallEngine {
678
678
 
679
679
  stopRingback()
680
680
  stopRingtone()
681
- appContext?.let { cancelIncomingCallUI(it) }
681
+ cancelIncomingCallUI()
682
682
 
683
683
  if (currentCallId == callId) {
684
684
  currentCallId = activeCalls.filter { it.value.state != CallState.ENDED }.keys.firstOrNull()
@@ -694,7 +694,7 @@ object CallEngine {
694
694
  }
695
695
 
696
696
  if (activeCalls.isEmpty()) {
697
- appContext?.let { finalCleanup(it) }
697
+ finalCleanup()
698
698
  } else {
699
699
  updateForegroundNotification()
700
700
  }
@@ -717,11 +717,7 @@ object CallEngine {
717
717
 
718
718
  // --- Audio Management ---
719
719
  fun getAudioDevices(): AudioRoutesInfo {
720
- val context = appContext ?: run {
721
- Log.e(TAG, "getAudioDevices: appContext is not set. Returning default.")
722
- return AudioRoutesInfo(emptyArray(), "Unknown")
723
- }
724
-
720
+ val context = requireContext()
725
721
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
726
722
  Log.e(TAG, "getAudioDevices: AudioManager is null. Returning default.")
727
723
  return AudioRoutesInfo(emptyArray(), "Unknown")
@@ -764,7 +760,8 @@ object CallEngine {
764
760
  return result
765
761
  }
766
762
 
767
- fun setAudioRoute(context: Context, route: String) {
763
+ fun setAudioRoute(route: String) {
764
+ val context = requireContext()
768
765
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
769
766
  Log.d(TAG, "Attempting to set audio route to: $route. Current mode: ${audioManager?.mode}")
770
767
 
@@ -815,7 +812,7 @@ object CallEngine {
815
812
  }
816
813
  }
817
814
 
818
- private fun setInitialAudioRoute(context: Context, callType: String) {
815
+ private fun setInitialAudioRoute(callType: String) {
819
816
  val availableDevices = getAudioDevices()
820
817
 
821
818
  val defaultRoute = when {
@@ -826,10 +823,11 @@ object CallEngine {
826
823
  }
827
824
 
828
825
  Log.d(TAG, "Setting initial audio route for $callType call: $defaultRoute")
829
- setAudioRoute(context, defaultRoute)
826
+ setAudioRoute(defaultRoute)
830
827
  }
831
828
 
832
- fun resetAudioMode(context: Context) {
829
+ private fun resetAudioMode() {
830
+ val context = requireContext()
833
831
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
834
832
  if (activeCalls.isEmpty()) {
835
833
  Log.d(TAG, "Resetting audio mode to NORMAL as no active calls remain.")
@@ -856,14 +854,15 @@ object CallEngine {
856
854
  }
857
855
  }
858
856
 
859
- fun registerAudioDeviceCallback(context: Context) {
860
- appContext = context.applicationContext
857
+ fun registerAudioDeviceCallback() {
858
+ val context = requireContext()
861
859
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
862
860
  audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
863
861
  Log.d(TAG, "Audio device callback registered.")
864
862
  }
865
863
 
866
- fun unregisterAudioDeviceCallback(context: Context) {
864
+ fun unregisterAudioDeviceCallback() {
865
+ val context = requireContext()
867
866
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
868
867
  audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
869
868
  Log.d(TAG, "Audio device callback unregistered.")
@@ -886,7 +885,8 @@ object CallEngine {
886
885
  }
887
886
 
888
887
  // --- Screen Management ---
889
- fun keepScreenAwake(context: Context, keepAwake: Boolean) {
888
+ fun keepScreenAwake(keepAwake: Boolean) {
889
+ val context = requireContext()
890
890
  val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
891
891
  if (keepAwake) {
892
892
  if (wakeLock == null || !wakeLock!!.isHeld) {
@@ -943,7 +943,8 @@ object CallEngine {
943
943
  }
944
944
 
945
945
  // --- Notification Management ---
946
- private fun createNotificationChannel(context: Context) {
946
+ private fun createNotificationChannel() {
947
+ val context = requireContext()
947
948
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
948
949
  val channel = NotificationChannel(
949
950
  NOTIF_CHANNEL_ID,
@@ -974,9 +975,10 @@ object CallEngine {
974
975
  }
975
976
  }
976
977
 
977
- fun showIncomingCallUI(context: Context, callId: String, callerName: String, callType: String) {
978
+ private fun showIncomingCallUI(callId: String, callerName: String, callType: String) {
979
+ val context = requireContext()
978
980
  Log.d(TAG, "Showing incoming call UI for $callId, caller: $callerName, callType: $callType")
979
- createNotificationChannel(context)
981
+ createNotificationChannel()
980
982
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
981
983
 
982
984
  val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
@@ -1035,13 +1037,14 @@ object CallEngine {
1035
1037
  notificationManager.notify(NOTIF_ID, notification)
1036
1038
 
1037
1039
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
1038
- playRingtone(context)
1040
+ playRingtone()
1039
1041
  }
1040
1042
 
1041
- setInitialAudioRoute(context, callType)
1043
+ setInitialAudioRoute(callType)
1042
1044
  }
1043
1045
 
1044
- fun cancelIncomingCallUI(context: Context) {
1046
+ private fun cancelIncomingCallUI() {
1047
+ val context = requireContext()
1045
1048
  Log.d(TAG, "Cancelling incoming call UI.")
1046
1049
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
1047
1050
  notificationManager.cancel(NOTIF_ID)
@@ -1049,7 +1052,8 @@ object CallEngine {
1049
1052
  }
1050
1053
 
1051
1054
  // --- Service Management ---
1052
- fun startForegroundService(context: Context) {
1055
+ private fun startForegroundService() {
1056
+ val context = requireContext()
1053
1057
  Log.d(TAG, "Starting CallForegroundService.")
1054
1058
 
1055
1059
  val currentCall = activeCalls.values.find {
@@ -1074,13 +1078,15 @@ object CallEngine {
1074
1078
  }
1075
1079
  }
1076
1080
 
1077
- fun stopForegroundService(context: Context) {
1081
+ private fun stopForegroundService() {
1082
+ val context = requireContext()
1078
1083
  Log.d(TAG, "Stopping CallForegroundService.")
1079
1084
  val intent = Intent(context, CallForegroundService::class.java)
1080
1085
  context.stopService(intent)
1081
1086
  }
1082
1087
 
1083
- fun bringAppToForeground(context: Context) {
1088
+ private fun bringAppToForeground() {
1089
+ val context = requireContext()
1084
1090
  val packageName = context.packageName
1085
1091
  val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
1086
1092
  launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
@@ -1105,9 +1111,10 @@ object CallEngine {
1105
1111
  }
1106
1112
 
1107
1113
  // --- Phone Account Management ---
1108
- private fun registerPhoneAccount(context: Context) {
1114
+ private fun registerPhoneAccount() {
1115
+ val context = requireContext()
1109
1116
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
1110
- val phoneAccountHandle = getPhoneAccountHandle(context)
1117
+ val phoneAccountHandle = getPhoneAccountHandle()
1111
1118
 
1112
1119
  if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
1113
1120
  val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
@@ -1127,7 +1134,8 @@ object CallEngine {
1127
1134
  }
1128
1135
  }
1129
1136
 
1130
- private fun getPhoneAccountHandle(context: Context): PhoneAccountHandle {
1137
+ private fun getPhoneAccountHandle(): PhoneAccountHandle {
1138
+ val context = requireContext()
1131
1139
  return PhoneAccountHandle(
1132
1140
  ComponentName(context, MyConnectionService::class.java),
1133
1141
  PHONE_ACCOUNT_ID
@@ -1135,7 +1143,8 @@ object CallEngine {
1135
1143
  }
1136
1144
 
1137
1145
  // --- Media Management ---
1138
- fun playRingtone(context: Context) {
1146
+ private fun playRingtone() {
1147
+ val context = requireContext()
1139
1148
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1140
1149
  Log.d(TAG, "playRingtone: Android S+ detected, system will handle ringtone via Telecom.")
1141
1150
  return
@@ -1155,7 +1164,7 @@ object CallEngine {
1155
1164
  }
1156
1165
  }
1157
1166
 
1158
- fun stopRingtone() {
1167
+ private fun stopRingtone() {
1159
1168
  try {
1160
1169
  if (ringtone?.isPlaying == true) {
1161
1170
  ringtone?.stop()
@@ -1168,14 +1177,15 @@ object CallEngine {
1168
1177
  }
1169
1178
 
1170
1179
  private fun startRingback() {
1180
+ val context = requireContext()
1171
1181
  if (ringbackPlayer?.isPlaying == true) {
1172
1182
  Log.d(TAG, "Ringback tone already playing.")
1173
1183
  return
1174
1184
  }
1175
1185
 
1176
1186
  try {
1177
- val ringbackUri = Uri.parse("android.resource://${appContext?.packageName}/raw/ringback_tone")
1178
- ringbackPlayer = MediaPlayer.create(appContext, ringbackUri)
1187
+ val ringbackUri = Uri.parse("android.resource://${context.packageName}/raw/ringback_tone")
1188
+ ringbackPlayer = MediaPlayer.create(context, ringbackUri)
1179
1189
  if (ringbackPlayer == null) {
1180
1190
  Log.e(TAG, "Failed to create MediaPlayer for ringback. Check raw/ringback_tone.mp3 exists.")
1181
1191
  return
@@ -1212,7 +1222,7 @@ object CallEngine {
1212
1222
  }
1213
1223
 
1214
1224
  private fun updateForegroundNotification() {
1215
- val context = appContext ?: return
1225
+ val context = requireContext()
1216
1226
  val activeCall = activeCalls.values.find { it.state == CallState.ACTIVE }
1217
1227
  val heldCall = activeCalls.values.find { it.state == CallState.HELD }
1218
1228
 
@@ -1233,11 +1243,41 @@ object CallEngine {
1233
1243
  }
1234
1244
  }
1235
1245
 
1236
- private fun finalCleanup(context: Context) {
1246
+ private fun finalCleanup() {
1237
1247
  Log.d(TAG, "Performing final cleanup - no active calls remaining")
1238
- stopForegroundService(context)
1239
- keepScreenAwake(context, false)
1240
- resetAudioMode(context)
1248
+ stopForegroundService()
1249
+ keepScreenAwake(false)
1250
+ resetAudioMode()
1241
1251
  isSystemCallActive = false
1242
1252
  }
1253
+
1254
+ // --- Lifecycle Management ---
1255
+ fun onApplicationTerminate() {
1256
+ Log.d(TAG, "Application terminating - cleaning up all calls")
1257
+
1258
+ // End all calls properly
1259
+ activeCalls.keys.toList().forEach { callId ->
1260
+ val connection = telecomConnections[callId]
1261
+ connection?.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
1262
+ connection?.destroy()
1263
+ }
1264
+
1265
+ // Clear all state
1266
+ activeCalls.clear()
1267
+ telecomConnections.clear()
1268
+ callMetadata.clear()
1269
+ currentCallId = null
1270
+
1271
+ // Release resources
1272
+ finalCleanup()
1273
+
1274
+ // Clear callbacks
1275
+ lockScreenBypassCallbacks.clear()
1276
+ eventHandler = null
1277
+ cachedEvents.clear()
1278
+
1279
+ // Reset initialization
1280
+ isInitialized.set(false)
1281
+ appContext = null
1282
+ }
1243
1283
  }
@@ -172,7 +172,7 @@ class CallForegroundService : Service() {
172
172
 
173
173
  override fun onTaskRemoved(rootIntent: Intent?) {
174
174
  Log.d(TAG, "onTaskRemoved: User swiped app from recents. Ending all calls.")
175
- CallEngine.endAllCalls(this)
175
+ CallEngine.onApplicationTerminate()
176
176
  super.onTaskRemoved(rootIntent)
177
177
  stopSelf()
178
178
  }
@@ -181,5 +181,10 @@ class CallForegroundService : Service() {
181
181
  super.onDestroy()
182
182
  Log.d(TAG, "Service onDestroy. Stopping foreground.")
183
183
  stopForeground(true)
184
+
185
+ // Additional cleanup when service is destroyed
186
+ if (!CallEngine.isCallActive()) {
187
+ CallEngine.onApplicationTerminate()
188
+ }
184
189
  }
185
190
  }
@@ -8,55 +8,72 @@ class CallManager : HybridCallManagerSpec() {
8
8
 
9
9
  private val TAG = "CallManager"
10
10
 
11
+ init {
12
+ // Initialize CallEngine as soon as CallManager is created
13
+ try {
14
+ val context = com.facebook.react.bridge.ReactApplicationContext.getCurrentApplication()
15
+ if (context != null) {
16
+ CallEngine.initialize(context.applicationContext)
17
+ }
18
+ } catch (e: Exception) {
19
+ Log.w(TAG, "Could not initialize CallEngine in init, will try during first call")
20
+ }
21
+ }
22
+
23
+ private fun ensureInitialized() {
24
+ if (!CallEngine.isInitialized()) {
25
+ try {
26
+ val context = com.facebook.react.bridge.ReactApplicationContext.getCurrentApplication()
27
+ ?: throw IllegalStateException("No application context available")
28
+ CallEngine.initialize(context.applicationContext)
29
+ } catch (e: Exception) {
30
+ Log.e(TAG, "Failed to initialize CallEngine: ${e.message}", e)
31
+ throw IllegalStateException("CallEngine not initialized and cannot initialize: ${e.message}")
32
+ }
33
+ }
34
+ }
35
+
11
36
  override fun endCall(callId: String) {
12
37
  Log.d(TAG, "endCall requested for callId: $callId")
13
- CallEngine.getAppContext()?.let {
14
- CallEngine.endCall(it, callId)
15
- } ?: Log.e(TAG, "App context not set for endCall.")
38
+ ensureInitialized()
39
+ CallEngine.endCall(callId)
16
40
  }
17
41
 
18
42
  override fun endAllCalls() {
19
43
  Log.d(TAG, "endAllCalls requested")
20
- CallEngine.getAppContext()?.let {
21
- CallEngine.endAllCalls(it)
22
- } ?: Log.e(TAG, "App context not set for endAllCalls.")
44
+ ensureInitialized()
45
+ CallEngine.endAllCalls()
23
46
  }
24
47
 
25
48
  override fun silenceRingtone() {
26
49
  Log.d(TAG, "silenceRingtone requested.")
27
- CallEngine.getAppContext()?.let {
28
- CallEngine.stopRingtone()
29
- } ?: Log.e(TAG, "App context not set for silenceRingtone.")
50
+ ensureInitialized()
51
+ CallEngine.stopRingtone()
30
52
  }
31
53
 
32
54
  override fun getAudioDevices(): AudioRoutesInfo {
33
55
  Log.d(TAG, "getAudioDevices requested.")
34
- return CallEngine.getAppContext()?.let {
35
- CallEngine.getAudioDevices()
36
- } ?: run {
37
- Log.e(TAG, "App context not set for getAudioDevices. Returning empty AudioRoutesInfo.")
38
- AudioRoutesInfo(emptyArray(), "Unknown")
39
- }
56
+ ensureInitialized()
57
+ return CallEngine.getAudioDevices()
40
58
  }
41
59
 
42
60
  override fun setAudioRoute(route: String) {
43
61
  Log.d(TAG, "setAudioRoute requested for route: $route")
44
- CallEngine.getAppContext()?.let {
45
- CallEngine.setAudioRoute(it, route)
46
- } ?: Log.e(TAG, "App context not set for setAudioRoute.")
62
+ ensureInitialized()
63
+ CallEngine.setAudioRoute(route)
47
64
  }
48
65
 
49
66
  override fun keepScreenAwake(keepAwake: Boolean) {
50
67
  Log.d(TAG, "keepScreenAwake requested: $keepAwake")
51
- CallEngine.getAppContext()?.let {
52
- CallEngine.keepScreenAwake(it, keepAwake)
53
- } ?: Log.e(TAG, "App context not set for keepAwake.")
68
+ ensureInitialized()
69
+ CallEngine.keepScreenAwake(keepAwake)
54
70
  }
55
71
 
56
72
  override fun addListener(
57
73
  listener: (event: CallEventType, payload: String) -> Unit
58
74
  ): () -> Unit {
59
75
  Log.d(TAG, "addListener called with listener: $listener")
76
+ ensureInitialized()
60
77
  CallEngine.setEventHandler(listener)
61
78
  return {
62
79
  CallEngine.setEventHandler(null)
@@ -66,36 +83,31 @@ class CallManager : HybridCallManagerSpec() {
66
83
 
67
84
  override fun startOutgoingCall(callId: String, callType: String, targetName: String, metadata: String?) {
68
85
  Log.d(TAG, "startOutgoingCall requested: callId=$callId, callType=$callType, targetName=$targetName")
69
- CallEngine.getAppContext()?.let {
70
- CallEngine.startOutgoingCall(it, callId, callType, targetName, metadata)
71
- } ?: Log.e(TAG, "App context not set for startOutgoingCall.")
86
+ ensureInitialized()
87
+ CallEngine.startOutgoingCall(callId, callType, targetName, metadata)
72
88
  }
73
89
 
74
90
  override fun startCall(callId: String, callType: String, targetName: String, metadata: String?) {
75
91
  Log.d(TAG, "startCall requested: callId=$callId, callType=$callType, targetName=$targetName")
76
- CallEngine.getAppContext()?.let {
77
- CallEngine.startCall(it, callId, callType, targetName, metadata)
78
- } ?: Log.e(TAG, "App context not set for startCall.")
92
+ ensureInitialized()
93
+ CallEngine.startCall(callId, callType, targetName, metadata)
79
94
  }
80
95
 
81
96
  override fun callAnswered(callId: String) {
82
97
  Log.d(TAG, "callAnswered (from JS) requested for callId: $callId")
83
- CallEngine.getAppContext()?.let {
84
- CallEngine.callAnsweredFromJS(it, callId)
85
- } ?: Log.e(TAG, "App context not set for callAnswered.")
98
+ ensureInitialized()
99
+ CallEngine.callAnsweredFromJS(callId)
86
100
  }
87
101
 
88
102
  override fun setOnHold(callId: String, onHold: Boolean) {
89
103
  Log.d(TAG, "setOnHold requested for callId: $callId, onHold: $onHold")
90
- CallEngine.getAppContext()?.let {
91
- CallEngine.setOnHold(it, callId, onHold)
92
- } ?: Log.e(TAG, "App context not set for setOnHold.")
104
+ ensureInitialized()
105
+ CallEngine.setOnHold(callId, onHold)
93
106
  }
94
107
 
95
108
  override fun setMuted(callId: String, muted: Boolean) {
96
109
  Log.d(TAG, "setMuted requested for callId: $callId, muted: $muted")
97
- CallEngine.getAppContext()?.let {
98
- CallEngine.setMuted(it, callId, muted)
99
- } ?: Log.e(TAG, "App context not set for setMuted.")
110
+ ensureInitialized()
111
+ CallEngine.setMuted(callId, muted)
100
112
  }
101
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.56",
3
+ "version": "0.1.58",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",