@qusaieilouti99/call-manager 0.1.56 → 0.1.57

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,7 +1,6 @@
1
1
  package com.margelo.nitro.qusaieilouti99.callmanager
2
2
 
3
- import android.app.Activity
4
- import android.app.Notification
3
+ import android.app.Application
5
4
  import android.app.NotificationChannel
6
5
  import android.app.NotificationManager
7
6
  import android.app.PendingIntent
@@ -15,7 +14,6 @@ import android.media.AudioDeviceInfo
15
14
  import android.media.AudioFocusRequest
16
15
  import android.media.AudioManager
17
16
  import android.media.MediaPlayer
18
- import android.media.RingtoneManager
19
17
  import android.net.Uri
20
18
  import android.os.Build
21
19
  import android.os.Bundle
@@ -30,6 +28,9 @@ import android.telecom.PhoneAccountHandle
30
28
  import android.telecom.TelecomManager
31
29
  import android.telecom.VideoProfile
32
30
  import android.util.Log
31
+ import androidx.lifecycle.DefaultLifecycleObserver
32
+ import androidx.lifecycle.LifecycleOwner
33
+ import androidx.lifecycle.ProcessLifecycleOwner
33
34
  import kotlinx.coroutines.CoroutineScope
34
35
  import kotlinx.coroutines.Dispatchers
35
36
  import kotlinx.coroutines.launch
@@ -38,39 +39,40 @@ import org.json.JSONObject
38
39
  import java.util.concurrent.ConcurrentHashMap
39
40
  import java.util.concurrent.atomic.AtomicBoolean
40
41
 
41
- object CallEngine {
42
+ object CallEngine : DefaultLifecycleObserver {
42
43
  private const val TAG = "CallEngine"
43
44
  private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
44
45
  private const val NOTIF_CHANNEL_ID = "incoming_call_channel"
45
46
  private const val NOTIF_ID = 2001
46
- private const val FOREGROUND_CHANNEL_ID = "call_foreground_channel"
47
- private const val FOREGROUND_NOTIF_ID = 1001
48
47
 
49
- // Audio & Media
48
+ // Core Context Management - Single Source of Truth
49
+ @Volatile
50
+ private var appContext: Context? = null
51
+ private val isInitialized = AtomicBoolean(false)
52
+
53
+ // Audio Management
54
+ private var audioManager: AudioManager? = null
55
+ private var audioFocusRequest: AudioFocusRequest? = null
56
+ private var hasAudioFocus: Boolean = false
57
+ private var isSystemCallActive: Boolean = false
58
+ private var lastAudioRoutesInfo: AudioRoutesInfo? = null
59
+ private var lastMuteState: Boolean = false
60
+
61
+ // Media Management
50
62
  private var ringtone: android.media.Ringtone? = null
51
63
  private var ringbackPlayer: MediaPlayer? = null
52
- private var audioManager: AudioManager? = null
64
+
65
+ // Power Management
53
66
  private var wakeLock: PowerManager.WakeLock? = null
54
- private var appContext: Context? = null
55
- private var audioFocusRequest: AudioFocusRequest? = null
56
67
 
57
- // Call State Management - Only what native needs
68
+ // Call State Management - Single Source of Truth
58
69
  private val activeCalls = ConcurrentHashMap<String, CallInfo>()
59
70
  private val telecomConnections = ConcurrentHashMap<String, Connection>()
60
-
61
- // Separate opaque metadata storage - native doesn't interpret
62
71
  private val callMetadata = ConcurrentHashMap<String, String>()
63
-
64
72
  private var currentCallId: String? = null
65
73
  private var canMakeMultipleCalls: Boolean = false
66
74
 
67
- // Audio State Tracking
68
- private var lastAudioRoutesInfo: AudioRoutesInfo? = null
69
- private var lastMuteState: Boolean = false
70
- private var hasAudioFocus: Boolean = false
71
- private var isSystemCallActive: Boolean = false
72
-
73
- // Lock Screen Bypass
75
+ // Lock Screen Management
74
76
  private var lockScreenBypassActive = false
75
77
  private val lockScreenBypassCallbacks = mutableSetOf<LockScreenBypassCallback>()
76
78
 
@@ -78,14 +80,51 @@ object CallEngine {
78
80
  private var eventHandler: ((CallEventType, String) -> Unit)? = null
79
81
  private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
80
82
 
81
- // Operation State
82
- private val operationInProgress = AtomicBoolean(false)
83
+ // Service Management
84
+ private val serviceHandler = Handler(Looper.getMainLooper())
85
+ private var isServiceRunning = false
83
86
 
84
87
  interface LockScreenBypassCallback {
85
88
  fun onLockScreenBypassChanged(shouldBypass: Boolean)
86
89
  }
87
90
 
88
- // --- Audio Focus Management ---
91
+ /**
92
+ * MANDATORY: Initialize CallEngine with Application context
93
+ * This should be called from Application.onCreate() or MainApplication
94
+ */
95
+ fun initialize(application: Application) {
96
+ synchronized(this) {
97
+ if (isInitialized.get()) {
98
+ Log.d(TAG, "CallEngine already initialized")
99
+ return
100
+ }
101
+
102
+ appContext = application.applicationContext
103
+ audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
104
+
105
+ // Register lifecycle observer for proper cleanup
106
+ ProcessLifecycleOwner.get().lifecycle.addObserver(this)
107
+
108
+ // Register audio device callback
109
+ registerAudioDeviceCallback()
110
+
111
+ isInitialized.set(true)
112
+ Log.d(TAG, "CallEngine initialized successfully")
113
+ }
114
+ }
115
+
116
+ override fun onDestroy(owner: LifecycleOwner) {
117
+ Log.d(TAG, "Application lifecycle onDestroy - cleaning up all calls")
118
+ cleanup()
119
+ }
120
+
121
+ private fun ensureInitialized(): Context {
122
+ return appContext ?: throw IllegalStateException(
123
+ "CallEngine not initialized. Call CallEngine.initialize(application) from your Application.onCreate()"
124
+ )
125
+ }
126
+
127
+ // --- Audio Focus Management (Improved) ---
89
128
  private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
90
129
  Log.d(TAG, "Audio focus changed: $focusChange")
91
130
  when (focusChange) {
@@ -118,7 +157,7 @@ object CallEngine {
118
157
  hasAudioFocus = true
119
158
  isSystemCallActive = false
120
159
 
121
- Handler(Looper.getMainLooper()).postDelayed({
160
+ serviceHandler.postDelayed({
122
161
  activeCalls.values.filter { it.state == CallState.HELD && it.wasHeldBySystem }.forEach { call ->
123
162
  unholdCallInternal(call.callId, resumedBySystem = true)
124
163
  }
@@ -127,7 +166,7 @@ object CallEngine {
127
166
  }
128
167
 
129
168
  private fun requestAudioFocus(): Boolean {
130
- val context = appContext ?: return false
169
+ val context = ensureInitialized()
131
170
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
132
171
 
133
172
  return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -161,7 +200,7 @@ object CallEngine {
161
200
 
162
201
  private fun abandonAudioFocus() {
163
202
  val context = appContext ?: return
164
- audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
203
+ audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
165
204
 
166
205
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
167
206
  audioFocusRequest?.let { request ->
@@ -199,66 +238,83 @@ object CallEngine {
199
238
  }
200
239
  }
201
240
 
202
- // --- Lock Screen Bypass Management ---
203
- fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
204
- lockScreenBypassCallbacks.add(callback)
205
- }
206
-
207
- fun unregisterLockScreenBypassCallback(callback: LockScreenBypassCallback) {
208
- lockScreenBypassCallbacks.remove(callback)
209
- }
241
+ // --- Service Management (Fixed) ---
242
+ private fun startForegroundService(context: Context) {
243
+ if (isServiceRunning) {
244
+ Log.d(TAG, "Foreground service already running, updating notification")
245
+ updateForegroundNotification()
246
+ return
247
+ }
210
248
 
211
- private fun updateLockScreenBypass() {
212
- val shouldBypass = isCallActive()
213
- if (lockScreenBypassActive != shouldBypass) {
214
- lockScreenBypassActive = shouldBypass
215
- Log.d(TAG, "Lock screen bypass state changed: $lockScreenBypassActive")
216
- lockScreenBypassCallbacks.forEach { callback ->
217
- try {
218
- callback.onLockScreenBypassChanged(shouldBypass)
219
- } catch (e: Exception) {
220
- Log.w(TAG, "Error notifying lock screen bypass callback", e)
221
- }
222
- }
249
+ Log.d(TAG, "Starting CallForegroundService.")
250
+ val currentCall = activeCalls.values.find {
251
+ it.state == CallState.ACTIVE || it.state == CallState.INCOMING ||
252
+ it.state == CallState.DIALING || it.state == CallState.HELD
223
253
  }
224
- }
225
254
 
226
- fun isLockScreenBypassActive(): Boolean = lockScreenBypassActive
255
+ val intent = Intent(context, CallForegroundService::class.java)
256
+ if (currentCall != null) {
257
+ intent.putExtra("callId", currentCall.callId)
258
+ intent.putExtra("callType", currentCall.callType)
259
+ intent.putExtra("displayName", currentCall.displayName)
260
+ intent.putExtra("state", currentCall.state.name)
261
+ Log.d(TAG, "Starting foreground service with call info: ${currentCall.callId}")
262
+ }
227
263
 
228
- // --- Telecom Connection Management ---
229
- fun addTelecomConnection(callId: String, connection: Connection) {
230
- telecomConnections[callId] = connection
231
- Log.d(TAG, "Added Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
264
+ try {
265
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
266
+ context.startForegroundService(intent)
267
+ } else {
268
+ context.startService(intent)
269
+ }
270
+ isServiceRunning = true
271
+ Log.d(TAG, "Foreground service started successfully")
272
+ } catch (e: Exception) {
273
+ Log.e(TAG, "Failed to start foreground service: ${e.message}", e)
274
+ }
232
275
  }
233
276
 
234
- fun removeTelecomConnection(callId: String) {
235
- telecomConnections.remove(callId)?.let {
236
- Log.d(TAG, "Removed Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
277
+ private fun stopForegroundService(context: Context) {
278
+ if (!isServiceRunning) {
279
+ Log.d(TAG, "Foreground service not running")
280
+ return
237
281
  }
282
+
283
+ Log.d(TAG, "Stopping CallForegroundService.")
284
+ val intent = Intent(context, CallForegroundService::class.java)
285
+ context.stopService(intent)
286
+ isServiceRunning = false
238
287
  }
239
288
 
240
- fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
289
+ private fun updateForegroundNotification() {
290
+ val context = appContext ?: return
291
+ if (!isServiceRunning) return
241
292
 
242
- fun getAppContext(): Context? = appContext
293
+ val activeCall = activeCalls.values.find { it.state == CallState.ACTIVE }
294
+ val heldCall = activeCalls.values.find { it.state == CallState.HELD }
243
295
 
244
- // --- Public API ---
245
- fun setCanMakeMultipleCalls(allow: Boolean) {
246
- canMakeMultipleCalls = allow
247
- Log.d(TAG, "canMakeMultipleCalls set to: $allow")
248
- }
296
+ val callToShow = activeCall ?: heldCall
297
+ callToShow?.let {
298
+ val intent = Intent(context, CallForegroundService::class.java)
299
+ intent.putExtra("UPDATE_NOTIFICATION", true)
300
+ intent.putExtra("callId", it.callId)
301
+ intent.putExtra("callType", it.callType)
302
+ intent.putExtra("displayName", it.displayName)
303
+ intent.putExtra("state", it.state.name)
249
304
 
250
- fun getCurrentCallState(): String {
251
- val calls = getActiveCalls()
252
- val jsonArray = JSONArray()
253
- calls.forEach {
254
- jsonArray.put(it.toJsonObject())
305
+ try {
306
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
307
+ context.startForegroundService(intent)
308
+ } else {
309
+ context.startService(intent)
310
+ }
311
+ } catch (e: Exception) {
312
+ Log.e(TAG, "Failed to update foreground notification: ${e.message}", e)
313
+ }
255
314
  }
256
- val result = jsonArray.toString()
257
- Log.d(TAG, "Current call state: $result")
258
- return result
259
315
  }
260
316
 
261
- // --- Incoming Call Management ---
317
+ // --- Call Management (Fixed Context Issues) ---
262
318
  fun reportIncomingCall(
263
319
  context: Context,
264
320
  callId: String,
@@ -267,10 +323,9 @@ object CallEngine {
267
323
  pictureUrl: String? = null,
268
324
  metadata: String? = null
269
325
  ) {
270
- appContext = context.applicationContext
326
+ ensureInitialized()
271
327
  Log.d(TAG, "reportIncomingCall: callId=$callId, type=$callType, name=$displayName")
272
328
 
273
- // Store metadata separately if provided
274
329
  metadata?.let { callMetadata[callId] = it }
275
330
 
276
331
  // Check for call collision
@@ -302,6 +357,9 @@ object CallEngine {
302
357
  currentCallId = callId
303
358
  Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING, callType: $callType")
304
359
 
360
+ // Start foreground service FIRST
361
+ startForegroundService(context)
362
+
305
363
  showIncomingCallUI(context, callId, displayName, callType)
306
364
  registerPhoneAccount(context)
307
365
 
@@ -317,7 +375,6 @@ object CallEngine {
317
375
 
318
376
  try {
319
377
  telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
320
- startForegroundService(context)
321
378
  Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
322
379
  } catch (e: SecurityException) {
323
380
  Log.e(TAG, "SecurityException: Failed to report incoming call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
@@ -330,7 +387,6 @@ object CallEngine {
330
387
  updateLockScreenBypass()
331
388
  }
332
389
 
333
- // --- Outgoing Call Management ---
334
390
  fun startOutgoingCall(
335
391
  context: Context,
336
392
  callId: String,
@@ -338,7 +394,7 @@ object CallEngine {
338
394
  targetName: String,
339
395
  metadata: String? = null
340
396
  ) {
341
- appContext = context.applicationContext
397
+ ensureInitialized()
342
398
  Log.d(TAG, "startOutgoingCall: callId=$callId, type=$callType, target=$targetName")
343
399
 
344
400
  metadata?.let { callMetadata[callId] = it }
@@ -366,6 +422,9 @@ object CallEngine {
366
422
  currentCallId = callId
367
423
  Log.d(TAG, "Call $callId added to activeCalls. State: DIALING, callType: $callType")
368
424
 
425
+ // Start foreground service FIRST
426
+ startForegroundService(context)
427
+
369
428
  registerPhoneAccount(context)
370
429
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
371
430
  val phoneAccountHandle = getPhoneAccountHandle(context)
@@ -382,7 +441,6 @@ object CallEngine {
382
441
 
383
442
  try {
384
443
  telecomManager.placeCall(addressUri, extras)
385
- startForegroundService(context)
386
444
  Log.d(TAG, "Successfully reported outgoing call to TelecomManager via placeCall for $callId")
387
445
 
388
446
  requestAudioFocus()
@@ -401,7 +459,7 @@ object CallEngine {
401
459
  updateLockScreenBypass()
402
460
  }
403
461
 
404
- // NEW: Start call as active (not dialing)
462
+ // Fixed: startCall now properly initializes context and starts foreground service
405
463
  fun startCall(
406
464
  context: Context,
407
465
  callId: String,
@@ -409,7 +467,7 @@ object CallEngine {
409
467
  targetName: String,
410
468
  metadata: String? = null
411
469
  ) {
412
- appContext = context.applicationContext
470
+ ensureInitialized()
413
471
  Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
414
472
 
415
473
  metadata?.let { callMetadata[callId] = it }
@@ -432,10 +490,12 @@ object CallEngine {
432
490
  currentCallId = callId
433
491
  Log.d(TAG, "Call $callId started as ACTIVE, callType: $callType")
434
492
 
493
+ // Start foreground service FIRST
494
+ startForegroundService(context)
495
+
435
496
  registerPhoneAccount(context)
436
497
  requestAudioFocus()
437
498
  bringAppToForeground(context)
438
- startForegroundService(context)
439
499
  keepScreenAwake(context, true)
440
500
  setInitialAudioRoute(context, callType)
441
501
  updateLockScreenBypass()
@@ -444,13 +504,15 @@ object CallEngine {
444
504
  emitCallAnsweredWithMetadata(callId)
445
505
  }
446
506
 
447
- // --- Call Answer Management ---
507
+ // Fixed: Context management for all operations
448
508
  fun callAnsweredFromJS(context: Context, callId: String) {
509
+ ensureInitialized()
449
510
  Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
450
511
  coreCallAnswered(context, callId, isLocalAnswer = false)
451
512
  }
452
513
 
453
514
  fun answerCall(context: Context, callId: String) {
515
+ ensureInitialized()
454
516
  Log.d(TAG, "answerCall: $callId - local party answering")
455
517
  coreCallAnswered(context, callId, isLocalAnswer = true)
456
518
  }
@@ -513,13 +575,13 @@ object CallEngine {
513
575
  })
514
576
  }
515
577
 
516
- // --- Call Control Methods ---
517
578
  fun holdCall(context: Context, callId: String) {
579
+ ensureInitialized()
518
580
  holdCallInternal(callId, heldBySystem = false)
519
581
  }
520
582
 
521
- // NEW: Set hold state from JS
522
583
  fun setOnHold(context: Context, callId: String, onHold: Boolean) {
584
+ ensureInitialized()
523
585
  Log.d(TAG, "setOnHold: $callId, onHold: $onHold")
524
586
 
525
587
  val callInfo = activeCalls[callId]
@@ -561,6 +623,7 @@ object CallEngine {
561
623
  }
562
624
 
563
625
  fun unholdCall(context: Context, callId: String) {
626
+ ensureInitialized()
564
627
  unholdCallInternal(callId, resumedBySystem = false)
565
628
  }
566
629
 
@@ -572,10 +635,12 @@ object CallEngine {
572
635
  return
573
636
  }
574
637
 
638
+ // Fixed: Don't emit UNHOLD_FAILED for system-held calls or when we have focus
575
639
  if (!hasAudioFocus && !resumedBySystem) {
576
640
  Log.d(TAG, "Attempting to request audio focus for unhold")
577
641
  if (!requestAudioFocus()) {
578
642
  Log.w(TAG, "Failed to get audio focus for unhold")
643
+ // Only emit UNHOLD_FAILED for user-initiated unhold attempts
579
644
  emitEvent(CallEventType.CALL_UNHOLD_FAILED, JSONObject().apply {
580
645
  put("callId", callId)
581
646
  put("reason", "Could not obtain audio focus")
@@ -600,15 +665,17 @@ object CallEngine {
600
665
  }
601
666
 
602
667
  fun muteCall(context: Context, callId: String) {
668
+ ensureInitialized()
603
669
  setMutedInternal(context, callId, true)
604
670
  }
605
671
 
606
672
  fun unmuteCall(context: Context, callId: String) {
673
+ ensureInitialized()
607
674
  setMutedInternal(context, callId, false)
608
675
  }
609
676
 
610
- // NEW: Set mute state from JS
611
677
  fun setMuted(context: Context, callId: String, muted: Boolean) {
678
+ ensureInitialized()
612
679
  Log.d(TAG, "setMuted: $callId, muted: $muted")
613
680
  setMutedInternal(context, callId, muted)
614
681
  }
@@ -633,15 +700,14 @@ object CallEngine {
633
700
  }
634
701
  }
635
702
 
636
- // --- Call End Management ---
637
703
  fun endCall(context: Context, callId: String) {
638
- appContext = context.applicationContext
704
+ ensureInitialized()
639
705
  Log.d(TAG, "endCall: $callId")
640
706
  endCallInternal(callId)
641
707
  }
642
708
 
643
- // NEW: End all calls
644
709
  fun endAllCalls(context: Context) {
710
+ ensureInitialized()
645
711
  Log.d(TAG, "endAllCalls: Ending all active calls.")
646
712
  if (activeCalls.isEmpty()) {
647
713
  Log.d(TAG, "No active calls, nothing to do.")
@@ -715,13 +781,17 @@ object CallEngine {
715
781
  })
716
782
  }
717
783
 
784
+ private fun finalCleanup(context: Context) {
785
+ Log.d(TAG, "Performing final cleanup - no active calls remaining")
786
+ stopForegroundService(context)
787
+ keepScreenAwake(context, false)
788
+ resetAudioMode(context)
789
+ isSystemCallActive = false
790
+ }
791
+
718
792
  // --- Audio Management ---
719
793
  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
-
794
+ val context = ensureInitialized()
725
795
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
726
796
  Log.e(TAG, "getAudioDevices: AudioManager is null. Returning default.")
727
797
  return AudioRoutesInfo(emptyArray(), "Unknown")
@@ -765,6 +835,7 @@ object CallEngine {
765
835
  }
766
836
 
767
837
  fun setAudioRoute(context: Context, route: String) {
838
+ ensureInitialized()
768
839
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
769
840
  Log.d(TAG, "Attempting to set audio route to: $route. Current mode: ${audioManager?.mode}")
770
841
 
@@ -856,14 +927,15 @@ object CallEngine {
856
927
  }
857
928
  }
858
929
 
859
- fun registerAudioDeviceCallback(context: Context) {
860
- appContext = context.applicationContext
930
+ private fun registerAudioDeviceCallback() {
931
+ val context = appContext ?: return
861
932
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
862
933
  audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
863
934
  Log.d(TAG, "Audio device callback registered.")
864
935
  }
865
936
 
866
- fun unregisterAudioDeviceCallback(context: Context) {
937
+ private fun unregisterAudioDeviceCallback() {
938
+ val context = appContext ?: return
867
939
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
868
940
  audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
869
941
  Log.d(TAG, "Audio device callback unregistered.")
@@ -942,6 +1014,63 @@ object CallEngine {
942
1014
  })
943
1015
  }
944
1016
 
1017
+ // --- Lock Screen Bypass Management ---
1018
+ fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
1019
+ lockScreenBypassCallbacks.add(callback)
1020
+ }
1021
+
1022
+ fun unregisterLockScreenBypassCallback(callback: LockScreenBypassCallback) {
1023
+ lockScreenBypassCallbacks.remove(callback)
1024
+ }
1025
+
1026
+ private fun updateLockScreenBypass() {
1027
+ val shouldBypass = isCallActive()
1028
+ if (lockScreenBypassActive != shouldBypass) {
1029
+ lockScreenBypassActive = shouldBypass
1030
+ Log.d(TAG, "Lock screen bypass state changed: $lockScreenBypassActive")
1031
+ lockScreenBypassCallbacks.forEach { callback ->
1032
+ try {
1033
+ callback.onLockScreenBypassChanged(shouldBypass)
1034
+ } catch (e: Exception) {
1035
+ Log.w(TAG, "Error notifying lock screen bypass callback", e)
1036
+ }
1037
+ }
1038
+ }
1039
+ }
1040
+
1041
+ fun isLockScreenBypassActive(): Boolean = lockScreenBypassActive
1042
+
1043
+ // --- Telecom Connection Management ---
1044
+ fun addTelecomConnection(callId: String, connection: Connection) {
1045
+ telecomConnections[callId] = connection
1046
+ Log.d(TAG, "Added Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
1047
+ }
1048
+
1049
+ fun removeTelecomConnection(callId: String) {
1050
+ telecomConnections.remove(callId)?.let {
1051
+ Log.d(TAG, "Removed Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
1052
+ }
1053
+ }
1054
+
1055
+ fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
1056
+
1057
+ // --- Public API ---
1058
+ fun setCanMakeMultipleCalls(allow: Boolean) {
1059
+ canMakeMultipleCalls = allow
1060
+ Log.d(TAG, "canMakeMultipleCalls set to: $allow")
1061
+ }
1062
+
1063
+ fun getCurrentCallState(): String {
1064
+ val calls = getActiveCalls()
1065
+ val jsonArray = JSONArray()
1066
+ calls.forEach {
1067
+ jsonArray.put(it.toJsonObject())
1068
+ }
1069
+ val result = jsonArray.toString()
1070
+ Log.d(TAG, "Current call state: $result")
1071
+ return result
1072
+ }
1073
+
945
1074
  // --- Notification Management ---
946
1075
  private fun createNotificationChannel(context: Context) {
947
1076
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -957,7 +1086,7 @@ object CallEngine {
957
1086
 
958
1087
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
959
1088
  channel.setSound(
960
- RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE),
1089
+ android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_RINGTONE),
961
1090
  AudioAttributes.Builder()
962
1091
  .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
963
1092
  .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
@@ -1010,20 +1139,20 @@ object CallEngine {
1010
1139
 
1011
1140
  val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1012
1141
  val person = android.app.Person.Builder().setName(callerName).setImportant(true).build()
1013
- Notification.Builder(context, NOTIF_CHANNEL_ID)
1142
+ android.app.Notification.Builder(context, NOTIF_CHANNEL_ID)
1014
1143
  .setSmallIcon(android.R.drawable.sym_call_incoming)
1015
- .setStyle(Notification.CallStyle.forIncomingCall(person, declinePendingIntent, answerPendingIntent))
1144
+ .setStyle(android.app.Notification.CallStyle.forIncomingCall(person, declinePendingIntent, answerPendingIntent))
1016
1145
  .setFullScreenIntent(fullScreenPendingIntent, true)
1017
1146
  .setOngoing(true)
1018
1147
  .setAutoCancel(false)
1019
1148
  .build()
1020
1149
  } else {
1021
- Notification.Builder(context, NOTIF_CHANNEL_ID)
1150
+ android.app.Notification.Builder(context, NOTIF_CHANNEL_ID)
1022
1151
  .setSmallIcon(android.R.drawable.sym_call_incoming)
1023
1152
  .setContentTitle("Incoming Call")
1024
1153
  .setContentText(callerName)
1025
- .setPriority(Notification.PRIORITY_HIGH)
1026
- .setCategory(Notification.CATEGORY_CALL)
1154
+ .setPriority(android.app.Notification.PRIORITY_HIGH)
1155
+ .setCategory(android.app.Notification.CATEGORY_CALL)
1027
1156
  .setFullScreenIntent(fullScreenPendingIntent, true)
1028
1157
  .addAction(android.R.drawable.sym_action_call, "Answer", answerPendingIntent)
1029
1158
  .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Decline", declinePendingIntent)
@@ -1048,38 +1177,6 @@ object CallEngine {
1048
1177
  stopRingtone()
1049
1178
  }
1050
1179
 
1051
- // --- Service Management ---
1052
- fun startForegroundService(context: Context) {
1053
- Log.d(TAG, "Starting CallForegroundService.")
1054
-
1055
- val currentCall = activeCalls.values.find {
1056
- it.state == CallState.ACTIVE || it.state == CallState.INCOMING ||
1057
- it.state == CallState.DIALING || it.state == CallState.HELD
1058
- }
1059
-
1060
- val intent = Intent(context, CallForegroundService::class.java)
1061
-
1062
- if (currentCall != null) {
1063
- intent.putExtra("callId", currentCall.callId)
1064
- intent.putExtra("callType", currentCall.callType)
1065
- intent.putExtra("displayName", currentCall.displayName)
1066
- intent.putExtra("state", currentCall.state.name)
1067
- Log.d(TAG, "Starting foreground service with call info: ${currentCall.callId}")
1068
- }
1069
-
1070
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1071
- context.startForegroundService(intent)
1072
- } else {
1073
- context.startService(intent)
1074
- }
1075
- }
1076
-
1077
- fun stopForegroundService(context: Context) {
1078
- Log.d(TAG, "Stopping CallForegroundService.")
1079
- val intent = Intent(context, CallForegroundService::class.java)
1080
- context.stopService(intent)
1081
- }
1082
-
1083
1180
  fun bringAppToForeground(context: Context) {
1084
1181
  val packageName = context.packageName
1085
1182
  val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
@@ -1096,7 +1193,7 @@ object CallEngine {
1096
1193
 
1097
1194
  try {
1098
1195
  context.startActivity(launchIntent)
1099
- Handler(Looper.getMainLooper()).postDelayed({
1196
+ serviceHandler.postDelayed({
1100
1197
  updateLockScreenBypass()
1101
1198
  }, 100)
1102
1199
  } catch (e: Exception) {
@@ -1143,8 +1240,8 @@ object CallEngine {
1143
1240
 
1144
1241
  try {
1145
1242
  Log.d(TAG, "Playing ringtone (for Android < S).")
1146
- val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
1147
- ringtone = RingtoneManager.getRingtone(context, uri)
1243
+ val uri = android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_RINGTONE)
1244
+ ringtone = android.media.RingtoneManager.getRingtone(context, uri)
1148
1245
  ringtone?.audioAttributes = AudioAttributes.Builder()
1149
1246
  .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
1150
1247
  .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
@@ -1211,33 +1308,48 @@ object CallEngine {
1211
1308
  }
1212
1309
  }
1213
1310
 
1214
- private fun updateForegroundNotification() {
1215
- val context = appContext ?: return
1216
- val activeCall = activeCalls.values.find { it.state == CallState.ACTIVE }
1217
- val heldCall = activeCalls.values.find { it.state == CallState.HELD }
1311
+ // --- Cleanup ---
1312
+ private fun cleanup() {
1313
+ Log.d(TAG, "Cleaning up CallEngine")
1218
1314
 
1219
- val callToShow = activeCall ?: heldCall
1220
- callToShow?.let {
1221
- val intent = Intent(context, CallForegroundService::class.java)
1222
- intent.putExtra("UPDATE_NOTIFICATION", true)
1223
- intent.putExtra("callId", it.callId)
1224
- intent.putExtra("callType", it.callType)
1225
- intent.putExtra("displayName", it.displayName)
1226
- intent.putExtra("state", it.state.name)
1315
+ // End all calls
1316
+ activeCalls.keys.toList().forEach { callId ->
1317
+ endCallInternal(callId)
1318
+ }
1227
1319
 
1228
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1229
- context.startForegroundService(intent)
1230
- } else {
1231
- context.startService(intent)
1232
- }
1320
+ // Clear all collections
1321
+ activeCalls.clear()
1322
+ telecomConnections.clear()
1323
+ callMetadata.clear()
1324
+ lockScreenBypassCallbacks.clear()
1325
+ cachedEvents.clear()
1326
+
1327
+ // Stop media
1328
+ stopRingtone()
1329
+ stopRingback()
1330
+
1331
+ // Release resources
1332
+ wakeLock?.let {
1333
+ if (it.isHeld) it.release()
1233
1334
  }
1234
- }
1335
+ wakeLock = null
1235
1336
 
1236
- private fun finalCleanup(context: Context) {
1237
- Log.d(TAG, "Performing final cleanup - no active calls remaining")
1238
- stopForegroundService(context)
1239
- keepScreenAwake(context, false)
1240
- resetAudioMode(context)
1337
+ // Audio cleanup
1338
+ abandonAudioFocus()
1339
+ appContext?.let {
1340
+ resetAudioMode(it)
1341
+ unregisterAudioDeviceCallback()
1342
+ }
1343
+
1344
+ // Service cleanup
1345
+ appContext?.let { stopForegroundService(it) }
1346
+
1347
+ currentCallId = null
1348
+ lockScreenBypassActive = false
1349
+ hasAudioFocus = false
1241
1350
  isSystemCallActive = false
1351
+ isServiceRunning = false
1352
+
1353
+ Log.d(TAG, "CallEngine cleanup completed")
1242
1354
  }
1243
1355
  }
@@ -29,6 +29,7 @@ class CallForegroundService : Service() {
29
29
  override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
30
30
  Log.d(TAG, "Service onStartCommand")
31
31
 
32
+ val isUpdate = intent?.getBooleanExtra("UPDATE_NOTIFICATION", false) ?: false
32
33
  val callId = intent?.getStringExtra("callId")
33
34
  val callType = intent?.getStringExtra("callType")
34
35
  val displayName = intent?.getStringExtra("displayName")
@@ -42,7 +43,13 @@ class CallForegroundService : Service() {
42
43
  buildBasicNotification()
43
44
  }
44
45
 
45
- startForeground(NOTIFICATION_ID, notification)
46
+ if (!isUpdate) {
47
+ startForeground(NOTIFICATION_ID, notification)
48
+ } else {
49
+ val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
50
+ notificationManager.notify(NOTIFICATION_ID, notification)
51
+ }
52
+
46
53
  return START_STICKY
47
54
  }
48
55
 
@@ -172,7 +179,11 @@ class CallForegroundService : Service() {
172
179
 
173
180
  override fun onTaskRemoved(rootIntent: Intent?) {
174
181
  Log.d(TAG, "onTaskRemoved: User swiped app from recents. Ending all calls.")
175
- CallEngine.endAllCalls(this)
182
+ try {
183
+ CallEngine.endAllCalls(this)
184
+ } catch (e: Exception) {
185
+ Log.e(TAG, "Error ending calls on task removed: ${e.message}", e)
186
+ }
176
187
  super.onTaskRemoved(rootIntent)
177
188
  stopSelf()
178
189
  }
@@ -10,47 +10,57 @@ class CallManager : HybridCallManagerSpec() {
10
10
 
11
11
  override fun endCall(callId: String) {
12
12
  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.")
13
+ try {
14
+ val context = CallEngine.ensureInitialized()
15
+ CallEngine.endCall(context, callId)
16
+ } catch (e: IllegalStateException) {
17
+ Log.e(TAG, "CallEngine not initialized: ${e.message}")
18
+ }
16
19
  }
17
20
 
18
21
  override fun endAllCalls() {
19
22
  Log.d(TAG, "endAllCalls requested")
20
- CallEngine.getAppContext()?.let {
21
- CallEngine.endAllCalls(it)
22
- } ?: Log.e(TAG, "App context not set for endAllCalls.")
23
+ try {
24
+ val context = CallEngine.ensureInitialized()
25
+ CallEngine.endAllCalls(context)
26
+ } catch (e: IllegalStateException) {
27
+ Log.e(TAG, "CallEngine not initialized: ${e.message}")
28
+ }
23
29
  }
24
30
 
25
31
  override fun silenceRingtone() {
26
32
  Log.d(TAG, "silenceRingtone requested.")
27
- CallEngine.getAppContext()?.let {
28
- CallEngine.stopRingtone()
29
- } ?: Log.e(TAG, "App context not set for silenceRingtone.")
33
+ CallEngine.stopRingtone()
30
34
  }
31
35
 
32
36
  override fun getAudioDevices(): AudioRoutesInfo {
33
37
  Log.d(TAG, "getAudioDevices requested.")
34
- return CallEngine.getAppContext()?.let {
38
+ return try {
35
39
  CallEngine.getAudioDevices()
36
- } ?: run {
37
- Log.e(TAG, "App context not set for getAudioDevices. Returning empty AudioRoutesInfo.")
40
+ } catch (e: IllegalStateException) {
41
+ Log.e(TAG, "CallEngine not initialized: ${e.message}")
38
42
  AudioRoutesInfo(emptyArray(), "Unknown")
39
43
  }
40
44
  }
41
45
 
42
46
  override fun setAudioRoute(route: String) {
43
47
  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.")
48
+ try {
49
+ val context = CallEngine.ensureInitialized()
50
+ CallEngine.setAudioRoute(context, route)
51
+ } catch (e: IllegalStateException) {
52
+ Log.e(TAG, "CallEngine not initialized: ${e.message}")
53
+ }
47
54
  }
48
55
 
49
56
  override fun keepScreenAwake(keepAwake: Boolean) {
50
57
  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.")
58
+ try {
59
+ val context = CallEngine.ensureInitialized()
60
+ CallEngine.keepScreenAwake(context, keepAwake)
61
+ } catch (e: IllegalStateException) {
62
+ Log.e(TAG, "CallEngine not initialized: ${e.message}")
63
+ }
54
64
  }
55
65
 
56
66
  override fun addListener(
@@ -66,36 +76,51 @@ class CallManager : HybridCallManagerSpec() {
66
76
 
67
77
  override fun startOutgoingCall(callId: String, callType: String, targetName: String, metadata: String?) {
68
78
  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.")
79
+ try {
80
+ val context = CallEngine.ensureInitialized()
81
+ CallEngine.startOutgoingCall(context, callId, callType, targetName, metadata)
82
+ } catch (e: IllegalStateException) {
83
+ Log.e(TAG, "CallEngine not initialized: ${e.message}")
84
+ }
72
85
  }
73
86
 
74
87
  override fun startCall(callId: String, callType: String, targetName: String, metadata: String?) {
75
88
  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.")
89
+ try {
90
+ val context = CallEngine.ensureInitialized()
91
+ CallEngine.startCall(context, callId, callType, targetName, metadata)
92
+ } catch (e: IllegalStateException) {
93
+ Log.e(TAG, "CallEngine not initialized: ${e.message}")
94
+ }
79
95
  }
80
96
 
81
97
  override fun callAnswered(callId: String) {
82
98
  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.")
99
+ try {
100
+ val context = CallEngine.ensureInitialized()
101
+ CallEngine.callAnsweredFromJS(context, callId)
102
+ } catch (e: IllegalStateException) {
103
+ Log.e(TAG, "CallEngine not initialized: ${e.message}")
104
+ }
86
105
  }
87
106
 
88
107
  override fun setOnHold(callId: String, onHold: Boolean) {
89
108
  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.")
109
+ try {
110
+ val context = CallEngine.ensureInitialized()
111
+ CallEngine.setOnHold(context, callId, onHold)
112
+ } catch (e: IllegalStateException) {
113
+ Log.e(TAG, "CallEngine not initialized: ${e.message}")
114
+ }
93
115
  }
94
116
 
95
117
  override fun setMuted(callId: String, muted: Boolean) {
96
118
  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.")
119
+ try {
120
+ val context = CallEngine.ensureInitialized()
121
+ CallEngine.setMuted(context, callId, muted)
122
+ } catch (e: IllegalStateException) {
123
+ Log.e(TAG, "CallEngine not initialized: ${e.message}")
124
+ }
100
125
  }
101
126
  }
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.57",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",