@qusaieilouti99/call-manager 0.1.57 → 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,8 @@
1
1
  package com.margelo.nitro.qusaieilouti99.callmanager
2
2
 
3
+ import android.app.Activity
3
4
  import android.app.Application
5
+ import android.app.Notification
4
6
  import android.app.NotificationChannel
5
7
  import android.app.NotificationManager
6
8
  import android.app.PendingIntent
@@ -14,6 +16,7 @@ import android.media.AudioDeviceInfo
14
16
  import android.media.AudioFocusRequest
15
17
  import android.media.AudioManager
16
18
  import android.media.MediaPlayer
19
+ import android.media.RingtoneManager
17
20
  import android.net.Uri
18
21
  import android.os.Build
19
22
  import android.os.Bundle
@@ -28,9 +31,6 @@ import android.telecom.PhoneAccountHandle
28
31
  import android.telecom.TelecomManager
29
32
  import android.telecom.VideoProfile
30
33
  import android.util.Log
31
- import androidx.lifecycle.DefaultLifecycleObserver
32
- import androidx.lifecycle.LifecycleOwner
33
- import androidx.lifecycle.ProcessLifecycleOwner
34
34
  import kotlinx.coroutines.CoroutineScope
35
35
  import kotlinx.coroutines.Dispatchers
36
36
  import kotlinx.coroutines.launch
@@ -39,40 +39,39 @@ import org.json.JSONObject
39
39
  import java.util.concurrent.ConcurrentHashMap
40
40
  import java.util.concurrent.atomic.AtomicBoolean
41
41
 
42
- object CallEngine : DefaultLifecycleObserver {
42
+ object CallEngine {
43
43
  private const val TAG = "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
47
49
 
48
- // Core Context Management - Single Source of Truth
49
- @Volatile
50
+ // Core context - initialized once and maintained
50
51
  private var appContext: Context? = null
51
- private val isInitialized = AtomicBoolean(false)
52
+ private var isInitialized = AtomicBoolean(false)
52
53
 
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
54
+ // Audio & Media
62
55
  private var ringtone: android.media.Ringtone? = null
63
56
  private var ringbackPlayer: MediaPlayer? = null
64
-
65
- // Power Management
57
+ private var audioManager: AudioManager? = null
66
58
  private var wakeLock: PowerManager.WakeLock? = null
59
+ private var audioFocusRequest: AudioFocusRequest? = null
67
60
 
68
- // Call State Management - Single Source of Truth
61
+ // Call State Management
69
62
  private val activeCalls = ConcurrentHashMap<String, CallInfo>()
70
63
  private val telecomConnections = ConcurrentHashMap<String, Connection>()
71
64
  private val callMetadata = ConcurrentHashMap<String, String>()
65
+
72
66
  private var currentCallId: String? = null
73
67
  private var canMakeMultipleCalls: Boolean = false
74
68
 
75
- // Lock Screen Management
69
+ // Audio State Tracking
70
+ private var lastAudioRoutesInfo: AudioRoutesInfo? = null
71
+ private var hasAudioFocus: Boolean = false
72
+ private var isSystemCallActive: Boolean = false
73
+
74
+ // Lock Screen Bypass
76
75
  private var lockScreenBypassActive = false
77
76
  private val lockScreenBypassCallbacks = mutableSetOf<LockScreenBypassCallback>()
78
77
 
@@ -80,93 +79,92 @@ object CallEngine : DefaultLifecycleObserver {
80
79
  private var eventHandler: ((CallEventType, String) -> Unit)? = null
81
80
  private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
82
81
 
83
- // Service Management
84
- private val serviceHandler = Handler(Looper.getMainLooper())
85
- private var isServiceRunning = false
86
-
87
82
  interface LockScreenBypassCallback {
88
83
  fun onLockScreenBypassChanged(shouldBypass: Boolean)
89
84
  }
90
85
 
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
86
+ // --- INITIALIZATION - Fix for context management ---
87
+ fun initialize(context: Context) {
88
+ if (isInitialized.compareAndSet(false, true)) {
89
+ appContext = context.applicationContext
103
90
  audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
91
+ Log.d(TAG, "CallEngine initialized with context")
104
92
 
105
- // Register lifecycle observer for proper cleanup
106
- ProcessLifecycleOwner.get().lifecycle.addObserver(this)
93
+ // Initialize foreground service if needed
94
+ if (isCallActive()) {
95
+ startForegroundService()
96
+ }
97
+ }
98
+ }
107
99
 
108
- // Register audio device callback
109
- registerAudioDeviceCallback()
100
+ fun isInitialized(): Boolean = isInitialized.get()
110
101
 
111
- isInitialized.set(true)
112
- Log.d(TAG, "CallEngine initialized successfully")
113
- }
102
+ private fun requireContext(): Context {
103
+ return appContext ?: throw IllegalStateException("CallEngine not initialized. Call initialize() first.")
114
104
  }
115
105
 
116
- override fun onDestroy(owner: LifecycleOwner) {
117
- Log.d(TAG, "Application lifecycle onDestroy - cleaning up all calls")
118
- cleanup()
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
+ }
119
117
  }
120
118
 
121
- private fun ensureInitialized(): Context {
122
- return appContext ?: throw IllegalStateException(
123
- "CallEngine not initialized. Call CallEngine.initialize(application) from your Application.onCreate()"
124
- )
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
+ }
125
128
  }
126
129
 
127
- // --- Audio Focus Management (Improved) ---
130
+ // --- Audio Focus Management (Simplified) ---
128
131
  private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
129
132
  Log.d(TAG, "Audio focus changed: $focusChange")
130
133
  when (focusChange) {
131
- AudioManager.AUDIOFOCUS_LOSS -> handleAudioFocusLoss()
132
- AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> handleAudioFocusLoss()
133
- AudioManager.AUDIOFOCUS_GAIN -> handleAudioFocusGain()
134
- AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
135
- 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)
136
146
  }
137
147
  }
148
+ updateForegroundNotification()
138
149
  }
139
150
 
140
- private fun handleAudioFocusLoss() {
141
- Log.d(TAG, "Audio focus lost - likely system call active")
142
- hasAudioFocus = false
143
- isSystemCallActive = true
144
-
151
+ private fun holdSystemCalls() {
145
152
  activeCalls.values.filter { it.state == CallState.ACTIVE }.forEach { call ->
146
153
  if (!call.wasHeldBySystem) {
147
154
  holdCallInternal(call.callId, heldBySystem = true)
148
155
  }
149
156
  }
150
-
151
157
  stopRingback()
152
- updateForegroundNotification()
153
158
  }
154
159
 
155
- private fun handleAudioFocusGain() {
156
- Log.d(TAG, "Audio focus regained - system call likely ended")
157
- hasAudioFocus = true
158
- isSystemCallActive = false
159
-
160
- serviceHandler.postDelayed({
161
- activeCalls.values.filter { it.state == CallState.HELD && it.wasHeldBySystem }.forEach { call ->
162
- unholdCallInternal(call.callId, resumedBySystem = true)
163
- }
164
- updateForegroundNotification()
165
- }, 1000)
160
+ private fun resumeSystemHeldCalls() {
161
+ activeCalls.values.filter { it.state == CallState.HELD && it.wasHeldBySystem }.forEach { call ->
162
+ unholdCallInternal(call.callId, resumedBySystem = true)
163
+ }
166
164
  }
167
165
 
168
166
  private fun requestAudioFocus(): Boolean {
169
- val context = ensureInitialized()
167
+ val context = requireContext()
170
168
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
171
169
 
172
170
  return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -199,122 +197,78 @@ object CallEngine : DefaultLifecycleObserver {
199
197
  }
200
198
 
201
199
  private fun abandonAudioFocus() {
202
- val context = appContext ?: return
203
- audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
204
-
205
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
206
- audioFocusRequest?.let { request ->
207
- 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)
208
208
  }
209
- } else {
210
- @Suppress("DEPRECATION")
211
- audioManager?.abandonAudioFocus(audioFocusChangeListener)
212
209
  }
213
210
  hasAudioFocus = false
214
211
  Log.d(TAG, "Audio focus abandoned")
215
212
  }
216
213
 
217
- // --- Event System ---
218
- fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
219
- Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
220
- eventHandler = handler
221
- handler?.let { h ->
222
- if (cachedEvents.isNotEmpty()) {
223
- Log.d(TAG, "Emitting ${cachedEvents.size} cached events.")
224
- cachedEvents.forEach { (type, data) -> h.invoke(type, data) }
225
- cachedEvents.clear()
226
- }
227
- }
214
+ // --- Lock Screen Bypass Management ---
215
+ fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
216
+ lockScreenBypassCallbacks.add(callback)
228
217
  }
229
218
 
230
- fun emitEvent(type: CallEventType, data: JSONObject) {
231
- Log.d(TAG, "Emitting event: $type, data: $data")
232
- val dataString = data.toString()
233
- if (eventHandler != null) {
234
- eventHandler?.invoke(type, dataString)
235
- } else {
236
- Log.d(TAG, "No event handler registered, caching event: $type")
237
- cachedEvents.add(Pair(type, dataString))
238
- }
219
+ fun unregisterLockScreenBypassCallback(callback: LockScreenBypassCallback) {
220
+ lockScreenBypassCallbacks.remove(callback)
239
221
  }
240
222
 
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
- }
248
-
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
253
- }
254
-
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
- }
263
-
264
- try {
265
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
266
- context.startForegroundService(intent)
267
- } else {
268
- context.startService(intent)
223
+ private fun updateLockScreenBypass() {
224
+ val shouldBypass = isCallActive()
225
+ if (lockScreenBypassActive != shouldBypass) {
226
+ lockScreenBypassActive = shouldBypass
227
+ Log.d(TAG, "Lock screen bypass state changed: $lockScreenBypassActive")
228
+ lockScreenBypassCallbacks.forEach { callback ->
229
+ try {
230
+ callback.onLockScreenBypassChanged(shouldBypass)
231
+ } catch (e: Exception) {
232
+ Log.w(TAG, "Error notifying lock screen bypass callback", e)
233
+ }
269
234
  }
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
235
  }
275
236
  }
276
237
 
277
- private fun stopForegroundService(context: Context) {
278
- if (!isServiceRunning) {
279
- Log.d(TAG, "Foreground service not running")
280
- return
281
- }
238
+ fun isLockScreenBypassActive(): Boolean = lockScreenBypassActive
282
239
 
283
- Log.d(TAG, "Stopping CallForegroundService.")
284
- val intent = Intent(context, CallForegroundService::class.java)
285
- context.stopService(intent)
286
- isServiceRunning = false
240
+ // --- Telecom Connection Management ---
241
+ fun addTelecomConnection(callId: String, connection: Connection) {
242
+ telecomConnections[callId] = connection
243
+ Log.d(TAG, "Added Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
287
244
  }
288
245
 
289
- private fun updateForegroundNotification() {
290
- val context = appContext ?: return
291
- if (!isServiceRunning) return
246
+ fun removeTelecomConnection(callId: String) {
247
+ telecomConnections.remove(callId)?.let {
248
+ Log.d(TAG, "Removed Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
249
+ }
250
+ }
292
251
 
293
- val activeCall = activeCalls.values.find { it.state == CallState.ACTIVE }
294
- val heldCall = activeCalls.values.find { it.state == CallState.HELD }
252
+ fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
295
253
 
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)
254
+ // --- Public API ---
255
+ fun setCanMakeMultipleCalls(allow: Boolean) {
256
+ canMakeMultipleCalls = allow
257
+ Log.d(TAG, "canMakeMultipleCalls set to: $allow")
258
+ }
304
259
 
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
- }
260
+ fun getCurrentCallState(): String {
261
+ val calls = getActiveCalls()
262
+ val jsonArray = JSONArray()
263
+ calls.forEach {
264
+ jsonArray.put(it.toJsonObject())
314
265
  }
266
+ val result = jsonArray.toString()
267
+ Log.d(TAG, "Current call state: $result")
268
+ return result
315
269
  }
316
270
 
317
- // --- Call Management (Fixed Context Issues) ---
271
+ // --- Incoming Call Management ---
318
272
  fun reportIncomingCall(
319
273
  context: Context,
320
274
  callId: String,
@@ -323,7 +277,10 @@ object CallEngine : DefaultLifecycleObserver {
323
277
  pictureUrl: String? = null,
324
278
  metadata: String? = null
325
279
  ) {
326
- ensureInitialized()
280
+ if (!isInitialized.get()) {
281
+ initialize(context)
282
+ }
283
+
327
284
  Log.d(TAG, "reportIncomingCall: callId=$callId, type=$callType, name=$displayName")
328
285
 
329
286
  metadata?.let { callMetadata[callId] = it }
@@ -357,14 +314,11 @@ object CallEngine : DefaultLifecycleObserver {
357
314
  currentCallId = callId
358
315
  Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING, callType: $callType")
359
316
 
360
- // Start foreground service FIRST
361
- startForegroundService(context)
317
+ showIncomingCallUI(callId, displayName, callType)
318
+ registerPhoneAccount()
362
319
 
363
- showIncomingCallUI(context, callId, displayName, callType)
364
- registerPhoneAccount(context)
365
-
366
- val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
367
- val phoneAccountHandle = getPhoneAccountHandle(context)
320
+ val telecomManager = requireContext().getSystemService(Context.TELECOM_SERVICE) as TelecomManager
321
+ val phoneAccountHandle = getPhoneAccountHandle()
368
322
  val extras = Bundle().apply {
369
323
  putString(MyConnectionService.EXTRA_CALL_ID, callId)
370
324
  putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
@@ -375,6 +329,7 @@ object CallEngine : DefaultLifecycleObserver {
375
329
 
376
330
  try {
377
331
  telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
332
+ startForegroundService() // Fixed: Always start foreground service
378
333
  Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
379
334
  } catch (e: SecurityException) {
380
335
  Log.e(TAG, "SecurityException: Failed to report incoming call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
@@ -387,14 +342,14 @@ object CallEngine : DefaultLifecycleObserver {
387
342
  updateLockScreenBypass()
388
343
  }
389
344
 
345
+ // --- Outgoing Call Management ---
390
346
  fun startOutgoingCall(
391
- context: Context,
392
347
  callId: String,
393
348
  callType: String,
394
349
  targetName: String,
395
350
  metadata: String? = null
396
351
  ) {
397
- ensureInitialized()
352
+ val context = requireContext()
398
353
  Log.d(TAG, "startOutgoingCall: callId=$callId, type=$callType, target=$targetName")
399
354
 
400
355
  metadata?.let { callMetadata[callId] = it }
@@ -422,12 +377,9 @@ object CallEngine : DefaultLifecycleObserver {
422
377
  currentCallId = callId
423
378
  Log.d(TAG, "Call $callId added to activeCalls. State: DIALING, callType: $callType")
424
379
 
425
- // Start foreground service FIRST
426
- startForegroundService(context)
427
-
428
- registerPhoneAccount(context)
380
+ registerPhoneAccount()
429
381
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
430
- val phoneAccountHandle = getPhoneAccountHandle(context)
382
+ val phoneAccountHandle = getPhoneAccountHandle()
431
383
  val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
432
384
 
433
385
  val extras = Bundle().apply {
@@ -441,13 +393,13 @@ object CallEngine : DefaultLifecycleObserver {
441
393
 
442
394
  try {
443
395
  telecomManager.placeCall(addressUri, extras)
444
- Log.d(TAG, "Successfully reported outgoing call to TelecomManager via placeCall for $callId")
445
-
396
+ startForegroundService() // Fixed: Always start foreground service
446
397
  requestAudioFocus()
447
398
  startRingback()
448
- bringAppToForeground(context)
449
- keepScreenAwake(context, true)
450
- 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")
451
403
  } catch (e: SecurityException) {
452
404
  Log.e(TAG, "SecurityException: Failed to start outgoing call via placeCall. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
453
405
  endCallInternal(callId)
@@ -459,15 +411,14 @@ object CallEngine : DefaultLifecycleObserver {
459
411
  updateLockScreenBypass()
460
412
  }
461
413
 
462
- // Fixed: startCall now properly initializes context and starts foreground service
414
+ // Fixed: Start call as active (not dialing) with foreground service
463
415
  fun startCall(
464
- context: Context,
465
416
  callId: String,
466
417
  callType: String,
467
418
  targetName: String,
468
419
  metadata: String? = null
469
420
  ) {
470
- ensureInitialized()
421
+ val context = requireContext()
471
422
  Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
472
423
 
473
424
  metadata?.let { callMetadata[callId] = it }
@@ -490,34 +441,31 @@ object CallEngine : DefaultLifecycleObserver {
490
441
  currentCallId = callId
491
442
  Log.d(TAG, "Call $callId started as ACTIVE, callType: $callType")
492
443
 
493
- // Start foreground service FIRST
494
- startForegroundService(context)
495
-
496
- registerPhoneAccount(context)
444
+ registerPhoneAccount()
497
445
  requestAudioFocus()
498
- bringAppToForeground(context)
499
- keepScreenAwake(context, true)
500
- setInitialAudioRoute(context, callType)
446
+ bringAppToForeground()
447
+ startForegroundService() // Fixed: Start foreground service for JS-initiated calls
448
+ keepScreenAwake(true)
449
+ setInitialAudioRoute(callType)
501
450
  updateLockScreenBypass()
502
451
 
503
452
  // Emit call answered event with metadata
504
453
  emitCallAnsweredWithMetadata(callId)
505
454
  }
506
455
 
507
- // Fixed: Context management for all operations
508
- fun callAnsweredFromJS(context: Context, callId: String) {
509
- ensureInitialized()
456
+ // --- Call Answer Management ---
457
+ fun callAnsweredFromJS(callId: String) {
510
458
  Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
511
- coreCallAnswered(context, callId, isLocalAnswer = false)
459
+ coreCallAnswered(callId, isLocalAnswer = false)
512
460
  }
513
461
 
514
- fun answerCall(context: Context, callId: String) {
515
- ensureInitialized()
462
+ fun answerCall(callId: String) {
516
463
  Log.d(TAG, "answerCall: $callId - local party answering")
517
- coreCallAnswered(context, callId, isLocalAnswer = true)
464
+ coreCallAnswered(callId, isLocalAnswer = true)
518
465
  }
519
466
 
520
- private fun coreCallAnswered(context: Context, callId: String, isLocalAnswer: Boolean) {
467
+ private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
468
+ val context = requireContext()
521
469
  Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
522
470
 
523
471
  val callInfo = activeCalls[callId]
@@ -528,7 +476,7 @@ object CallEngine : DefaultLifecycleObserver {
528
476
 
529
477
  stopRingtone()
530
478
  stopRingback()
531
- cancelIncomingCallUI(context)
479
+ cancelIncomingCallUI()
532
480
  requestAudioFocus()
533
481
 
534
482
  activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
@@ -542,10 +490,10 @@ object CallEngine : DefaultLifecycleObserver {
542
490
  }
543
491
  }
544
492
 
545
- bringAppToForeground(context)
546
- startForegroundService(context)
547
- keepScreenAwake(context, true)
548
- resetAudioMode(context)
493
+ bringAppToForeground()
494
+ startForegroundService() // Fixed: Ensure foreground service is running
495
+ keepScreenAwake(true)
496
+ resetAudioMode()
549
497
  updateLockScreenBypass()
550
498
  updateForegroundNotification()
551
499
 
@@ -575,13 +523,12 @@ object CallEngine : DefaultLifecycleObserver {
575
523
  })
576
524
  }
577
525
 
578
- fun holdCall(context: Context, callId: String) {
579
- ensureInitialized()
526
+ // --- Call Control Methods ---
527
+ fun holdCall(callId: String) {
580
528
  holdCallInternal(callId, heldBySystem = false)
581
529
  }
582
530
 
583
- fun setOnHold(context: Context, callId: String, onHold: Boolean) {
584
- ensureInitialized()
531
+ fun setOnHold(callId: String, onHold: Boolean) {
585
532
  Log.d(TAG, "setOnHold: $callId, onHold: $onHold")
586
533
 
587
534
  val callInfo = activeCalls[callId]
@@ -622,8 +569,7 @@ object CallEngine : DefaultLifecycleObserver {
622
569
  updateLockScreenBypass()
623
570
  }
624
571
 
625
- fun unholdCall(context: Context, callId: String) {
626
- ensureInitialized()
572
+ fun unholdCall(callId: String) {
627
573
  unholdCallInternal(callId, resumedBySystem = false)
628
574
  }
629
575
 
@@ -635,18 +581,10 @@ object CallEngine : DefaultLifecycleObserver {
635
581
  return
636
582
  }
637
583
 
638
- // Fixed: Don't emit UNHOLD_FAILED for system-held calls or when we have focus
639
- if (!hasAudioFocus && !resumedBySystem) {
640
- Log.d(TAG, "Attempting to request audio focus for unhold")
641
- if (!requestAudioFocus()) {
642
- Log.w(TAG, "Failed to get audio focus for unhold")
643
- // Only emit UNHOLD_FAILED for user-initiated unhold attempts
644
- emitEvent(CallEventType.CALL_UNHOLD_FAILED, JSONObject().apply {
645
- put("callId", callId)
646
- put("reason", "Could not obtain audio focus")
647
- })
648
- return
649
- }
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
650
588
  }
651
589
 
652
590
  activeCalls[callId] = callInfo.copy(
@@ -664,23 +602,21 @@ object CallEngine : DefaultLifecycleObserver {
664
602
  Log.d(TAG, "Call $callId successfully unheld")
665
603
  }
666
604
 
667
- fun muteCall(context: Context, callId: String) {
668
- ensureInitialized()
669
- setMutedInternal(context, callId, true)
605
+ fun muteCall(callId: String) {
606
+ setMutedInternal(callId, true)
670
607
  }
671
608
 
672
- fun unmuteCall(context: Context, callId: String) {
673
- ensureInitialized()
674
- setMutedInternal(context, callId, false)
609
+ fun unmuteCall(callId: String) {
610
+ setMutedInternal(callId, false)
675
611
  }
676
612
 
677
- fun setMuted(context: Context, callId: String, muted: Boolean) {
678
- ensureInitialized()
613
+ fun setMuted(callId: String, muted: Boolean) {
679
614
  Log.d(TAG, "setMuted: $callId, muted: $muted")
680
- setMutedInternal(context, callId, muted)
615
+ setMutedInternal(callId, muted)
681
616
  }
682
617
 
683
- private fun setMutedInternal(context: Context, callId: String, muted: Boolean) {
618
+ private fun setMutedInternal(callId: String, muted: Boolean) {
619
+ val context = requireContext()
684
620
  val callInfo = activeCalls[callId]
685
621
  if (callInfo == null) {
686
622
  Log.w(TAG, "Cannot set mute state for call $callId - not found in active calls")
@@ -693,21 +629,19 @@ object CallEngine : DefaultLifecycleObserver {
693
629
  audioManager?.isMicrophoneMute = muted
694
630
 
695
631
  if (wasMuted != muted) {
696
- lastMuteState = muted
697
632
  val eventType = if (muted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
698
633
  emitEvent(eventType, JSONObject().put("callId", callId))
699
634
  Log.d(TAG, "Call $callId mute state changed to: $muted")
700
635
  }
701
636
  }
702
637
 
703
- fun endCall(context: Context, callId: String) {
704
- ensureInitialized()
638
+ // --- Call End Management ---
639
+ fun endCall(callId: String) {
705
640
  Log.d(TAG, "endCall: $callId")
706
641
  endCallInternal(callId)
707
642
  }
708
643
 
709
- fun endAllCalls(context: Context) {
710
- ensureInitialized()
644
+ fun endAllCalls() {
711
645
  Log.d(TAG, "endAllCalls: Ending all active calls.")
712
646
  if (activeCalls.isEmpty()) {
713
647
  Log.d(TAG, "No active calls, nothing to do.")
@@ -723,7 +657,7 @@ object CallEngine : DefaultLifecycleObserver {
723
657
  callMetadata.clear()
724
658
  currentCallId = null
725
659
 
726
- finalCleanup(context)
660
+ finalCleanup()
727
661
  updateLockScreenBypass()
728
662
  }
729
663
 
@@ -744,7 +678,7 @@ object CallEngine : DefaultLifecycleObserver {
744
678
 
745
679
  stopRingback()
746
680
  stopRingtone()
747
- appContext?.let { cancelIncomingCallUI(it) }
681
+ cancelIncomingCallUI()
748
682
 
749
683
  if (currentCallId == callId) {
750
684
  currentCallId = activeCalls.filter { it.value.state != CallState.ENDED }.keys.firstOrNull()
@@ -760,7 +694,7 @@ object CallEngine : DefaultLifecycleObserver {
760
694
  }
761
695
 
762
696
  if (activeCalls.isEmpty()) {
763
- appContext?.let { finalCleanup(it) }
697
+ finalCleanup()
764
698
  } else {
765
699
  updateForegroundNotification()
766
700
  }
@@ -781,17 +715,9 @@ object CallEngine : DefaultLifecycleObserver {
781
715
  })
782
716
  }
783
717
 
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
-
792
718
  // --- Audio Management ---
793
719
  fun getAudioDevices(): AudioRoutesInfo {
794
- val context = ensureInitialized()
720
+ val context = requireContext()
795
721
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
796
722
  Log.e(TAG, "getAudioDevices: AudioManager is null. Returning default.")
797
723
  return AudioRoutesInfo(emptyArray(), "Unknown")
@@ -834,8 +760,8 @@ object CallEngine : DefaultLifecycleObserver {
834
760
  return result
835
761
  }
836
762
 
837
- fun setAudioRoute(context: Context, route: String) {
838
- ensureInitialized()
763
+ fun setAudioRoute(route: String) {
764
+ val context = requireContext()
839
765
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
840
766
  Log.d(TAG, "Attempting to set audio route to: $route. Current mode: ${audioManager?.mode}")
841
767
 
@@ -886,7 +812,7 @@ object CallEngine : DefaultLifecycleObserver {
886
812
  }
887
813
  }
888
814
 
889
- private fun setInitialAudioRoute(context: Context, callType: String) {
815
+ private fun setInitialAudioRoute(callType: String) {
890
816
  val availableDevices = getAudioDevices()
891
817
 
892
818
  val defaultRoute = when {
@@ -897,10 +823,11 @@ object CallEngine : DefaultLifecycleObserver {
897
823
  }
898
824
 
899
825
  Log.d(TAG, "Setting initial audio route for $callType call: $defaultRoute")
900
- setAudioRoute(context, defaultRoute)
826
+ setAudioRoute(defaultRoute)
901
827
  }
902
828
 
903
- fun resetAudioMode(context: Context) {
829
+ private fun resetAudioMode() {
830
+ val context = requireContext()
904
831
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
905
832
  if (activeCalls.isEmpty()) {
906
833
  Log.d(TAG, "Resetting audio mode to NORMAL as no active calls remain.")
@@ -927,15 +854,15 @@ object CallEngine : DefaultLifecycleObserver {
927
854
  }
928
855
  }
929
856
 
930
- private fun registerAudioDeviceCallback() {
931
- val context = appContext ?: return
857
+ fun registerAudioDeviceCallback() {
858
+ val context = requireContext()
932
859
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
933
860
  audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
934
861
  Log.d(TAG, "Audio device callback registered.")
935
862
  }
936
863
 
937
- private fun unregisterAudioDeviceCallback() {
938
- val context = appContext ?: return
864
+ fun unregisterAudioDeviceCallback() {
865
+ val context = requireContext()
939
866
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
940
867
  audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
941
868
  Log.d(TAG, "Audio device callback unregistered.")
@@ -958,7 +885,8 @@ object CallEngine : DefaultLifecycleObserver {
958
885
  }
959
886
 
960
887
  // --- Screen Management ---
961
- fun keepScreenAwake(context: Context, keepAwake: Boolean) {
888
+ fun keepScreenAwake(keepAwake: Boolean) {
889
+ val context = requireContext()
962
890
  val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
963
891
  if (keepAwake) {
964
892
  if (wakeLock == null || !wakeLock!!.isHeld) {
@@ -1014,65 +942,9 @@ object CallEngine : DefaultLifecycleObserver {
1014
942
  })
1015
943
  }
1016
944
 
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
-
1074
945
  // --- Notification Management ---
1075
- private fun createNotificationChannel(context: Context) {
946
+ private fun createNotificationChannel() {
947
+ val context = requireContext()
1076
948
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1077
949
  val channel = NotificationChannel(
1078
950
  NOTIF_CHANNEL_ID,
@@ -1086,7 +958,7 @@ object CallEngine : DefaultLifecycleObserver {
1086
958
 
1087
959
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
1088
960
  channel.setSound(
1089
- android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_RINGTONE),
961
+ RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE),
1090
962
  AudioAttributes.Builder()
1091
963
  .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
1092
964
  .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
@@ -1103,9 +975,10 @@ object CallEngine : DefaultLifecycleObserver {
1103
975
  }
1104
976
  }
1105
977
 
1106
- fun showIncomingCallUI(context: Context, callId: String, callerName: String, callType: String) {
978
+ private fun showIncomingCallUI(callId: String, callerName: String, callType: String) {
979
+ val context = requireContext()
1107
980
  Log.d(TAG, "Showing incoming call UI for $callId, caller: $callerName, callType: $callType")
1108
- createNotificationChannel(context)
981
+ createNotificationChannel()
1109
982
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
1110
983
 
1111
984
  val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
@@ -1139,20 +1012,20 @@ object CallEngine : DefaultLifecycleObserver {
1139
1012
 
1140
1013
  val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1141
1014
  val person = android.app.Person.Builder().setName(callerName).setImportant(true).build()
1142
- android.app.Notification.Builder(context, NOTIF_CHANNEL_ID)
1015
+ Notification.Builder(context, NOTIF_CHANNEL_ID)
1143
1016
  .setSmallIcon(android.R.drawable.sym_call_incoming)
1144
- .setStyle(android.app.Notification.CallStyle.forIncomingCall(person, declinePendingIntent, answerPendingIntent))
1017
+ .setStyle(Notification.CallStyle.forIncomingCall(person, declinePendingIntent, answerPendingIntent))
1145
1018
  .setFullScreenIntent(fullScreenPendingIntent, true)
1146
1019
  .setOngoing(true)
1147
1020
  .setAutoCancel(false)
1148
1021
  .build()
1149
1022
  } else {
1150
- android.app.Notification.Builder(context, NOTIF_CHANNEL_ID)
1023
+ Notification.Builder(context, NOTIF_CHANNEL_ID)
1151
1024
  .setSmallIcon(android.R.drawable.sym_call_incoming)
1152
1025
  .setContentTitle("Incoming Call")
1153
1026
  .setContentText(callerName)
1154
- .setPriority(android.app.Notification.PRIORITY_HIGH)
1155
- .setCategory(android.app.Notification.CATEGORY_CALL)
1027
+ .setPriority(Notification.PRIORITY_HIGH)
1028
+ .setCategory(Notification.CATEGORY_CALL)
1156
1029
  .setFullScreenIntent(fullScreenPendingIntent, true)
1157
1030
  .addAction(android.R.drawable.sym_action_call, "Answer", answerPendingIntent)
1158
1031
  .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Decline", declinePendingIntent)
@@ -1164,20 +1037,56 @@ object CallEngine : DefaultLifecycleObserver {
1164
1037
  notificationManager.notify(NOTIF_ID, notification)
1165
1038
 
1166
1039
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
1167
- playRingtone(context)
1040
+ playRingtone()
1168
1041
  }
1169
1042
 
1170
- setInitialAudioRoute(context, callType)
1043
+ setInitialAudioRoute(callType)
1171
1044
  }
1172
1045
 
1173
- fun cancelIncomingCallUI(context: Context) {
1046
+ private fun cancelIncomingCallUI() {
1047
+ val context = requireContext()
1174
1048
  Log.d(TAG, "Cancelling incoming call UI.")
1175
1049
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
1176
1050
  notificationManager.cancel(NOTIF_ID)
1177
1051
  stopRingtone()
1178
1052
  }
1179
1053
 
1180
- fun bringAppToForeground(context: Context) {
1054
+ // --- Service Management ---
1055
+ private fun startForegroundService() {
1056
+ val context = requireContext()
1057
+ Log.d(TAG, "Starting CallForegroundService.")
1058
+
1059
+ val currentCall = activeCalls.values.find {
1060
+ it.state == CallState.ACTIVE || it.state == CallState.INCOMING ||
1061
+ it.state == CallState.DIALING || it.state == CallState.HELD
1062
+ }
1063
+
1064
+ val intent = Intent(context, CallForegroundService::class.java)
1065
+
1066
+ if (currentCall != null) {
1067
+ intent.putExtra("callId", currentCall.callId)
1068
+ intent.putExtra("callType", currentCall.callType)
1069
+ intent.putExtra("displayName", currentCall.displayName)
1070
+ intent.putExtra("state", currentCall.state.name)
1071
+ Log.d(TAG, "Starting foreground service with call info: ${currentCall.callId}")
1072
+ }
1073
+
1074
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1075
+ context.startForegroundService(intent)
1076
+ } else {
1077
+ context.startService(intent)
1078
+ }
1079
+ }
1080
+
1081
+ private fun stopForegroundService() {
1082
+ val context = requireContext()
1083
+ Log.d(TAG, "Stopping CallForegroundService.")
1084
+ val intent = Intent(context, CallForegroundService::class.java)
1085
+ context.stopService(intent)
1086
+ }
1087
+
1088
+ private fun bringAppToForeground() {
1089
+ val context = requireContext()
1181
1090
  val packageName = context.packageName
1182
1091
  val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
1183
1092
  launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
@@ -1193,7 +1102,7 @@ object CallEngine : DefaultLifecycleObserver {
1193
1102
 
1194
1103
  try {
1195
1104
  context.startActivity(launchIntent)
1196
- serviceHandler.postDelayed({
1105
+ Handler(Looper.getMainLooper()).postDelayed({
1197
1106
  updateLockScreenBypass()
1198
1107
  }, 100)
1199
1108
  } catch (e: Exception) {
@@ -1202,9 +1111,10 @@ object CallEngine : DefaultLifecycleObserver {
1202
1111
  }
1203
1112
 
1204
1113
  // --- Phone Account Management ---
1205
- private fun registerPhoneAccount(context: Context) {
1114
+ private fun registerPhoneAccount() {
1115
+ val context = requireContext()
1206
1116
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
1207
- val phoneAccountHandle = getPhoneAccountHandle(context)
1117
+ val phoneAccountHandle = getPhoneAccountHandle()
1208
1118
 
1209
1119
  if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
1210
1120
  val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
@@ -1224,7 +1134,8 @@ object CallEngine : DefaultLifecycleObserver {
1224
1134
  }
1225
1135
  }
1226
1136
 
1227
- private fun getPhoneAccountHandle(context: Context): PhoneAccountHandle {
1137
+ private fun getPhoneAccountHandle(): PhoneAccountHandle {
1138
+ val context = requireContext()
1228
1139
  return PhoneAccountHandle(
1229
1140
  ComponentName(context, MyConnectionService::class.java),
1230
1141
  PHONE_ACCOUNT_ID
@@ -1232,7 +1143,8 @@ object CallEngine : DefaultLifecycleObserver {
1232
1143
  }
1233
1144
 
1234
1145
  // --- Media Management ---
1235
- fun playRingtone(context: Context) {
1146
+ private fun playRingtone() {
1147
+ val context = requireContext()
1236
1148
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1237
1149
  Log.d(TAG, "playRingtone: Android S+ detected, system will handle ringtone via Telecom.")
1238
1150
  return
@@ -1240,8 +1152,8 @@ object CallEngine : DefaultLifecycleObserver {
1240
1152
 
1241
1153
  try {
1242
1154
  Log.d(TAG, "Playing ringtone (for Android < S).")
1243
- val uri = android.media.RingtoneManager.getDefaultUri(android.media.RingtoneManager.TYPE_RINGTONE)
1244
- ringtone = android.media.RingtoneManager.getRingtone(context, uri)
1155
+ val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
1156
+ ringtone = RingtoneManager.getRingtone(context, uri)
1245
1157
  ringtone?.audioAttributes = AudioAttributes.Builder()
1246
1158
  .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
1247
1159
  .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
@@ -1252,7 +1164,7 @@ object CallEngine : DefaultLifecycleObserver {
1252
1164
  }
1253
1165
  }
1254
1166
 
1255
- fun stopRingtone() {
1167
+ private fun stopRingtone() {
1256
1168
  try {
1257
1169
  if (ringtone?.isPlaying == true) {
1258
1170
  ringtone?.stop()
@@ -1265,14 +1177,15 @@ object CallEngine : DefaultLifecycleObserver {
1265
1177
  }
1266
1178
 
1267
1179
  private fun startRingback() {
1180
+ val context = requireContext()
1268
1181
  if (ringbackPlayer?.isPlaying == true) {
1269
1182
  Log.d(TAG, "Ringback tone already playing.")
1270
1183
  return
1271
1184
  }
1272
1185
 
1273
1186
  try {
1274
- val ringbackUri = Uri.parse("android.resource://${appContext?.packageName}/raw/ringback_tone")
1275
- ringbackPlayer = MediaPlayer.create(appContext, ringbackUri)
1187
+ val ringbackUri = Uri.parse("android.resource://${context.packageName}/raw/ringback_tone")
1188
+ ringbackPlayer = MediaPlayer.create(context, ringbackUri)
1276
1189
  if (ringbackPlayer == null) {
1277
1190
  Log.e(TAG, "Failed to create MediaPlayer for ringback. Check raw/ringback_tone.mp3 exists.")
1278
1191
  return
@@ -1308,48 +1221,63 @@ object CallEngine : DefaultLifecycleObserver {
1308
1221
  }
1309
1222
  }
1310
1223
 
1311
- // --- Cleanup ---
1312
- private fun cleanup() {
1313
- Log.d(TAG, "Cleaning up CallEngine")
1224
+ private fun updateForegroundNotification() {
1225
+ val context = requireContext()
1226
+ val activeCall = activeCalls.values.find { it.state == CallState.ACTIVE }
1227
+ val heldCall = activeCalls.values.find { it.state == CallState.HELD }
1228
+
1229
+ val callToShow = activeCall ?: heldCall
1230
+ callToShow?.let {
1231
+ val intent = Intent(context, CallForegroundService::class.java)
1232
+ intent.putExtra("UPDATE_NOTIFICATION", true)
1233
+ intent.putExtra("callId", it.callId)
1234
+ intent.putExtra("callType", it.callType)
1235
+ intent.putExtra("displayName", it.displayName)
1236
+ intent.putExtra("state", it.state.name)
1237
+
1238
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1239
+ context.startForegroundService(intent)
1240
+ } else {
1241
+ context.startService(intent)
1242
+ }
1243
+ }
1244
+ }
1245
+
1246
+ private fun finalCleanup() {
1247
+ Log.d(TAG, "Performing final cleanup - no active calls remaining")
1248
+ stopForegroundService()
1249
+ keepScreenAwake(false)
1250
+ resetAudioMode()
1251
+ isSystemCallActive = false
1252
+ }
1253
+
1254
+ // --- Lifecycle Management ---
1255
+ fun onApplicationTerminate() {
1256
+ Log.d(TAG, "Application terminating - cleaning up all calls")
1314
1257
 
1315
- // End all calls
1258
+ // End all calls properly
1316
1259
  activeCalls.keys.toList().forEach { callId ->
1317
- endCallInternal(callId)
1260
+ val connection = telecomConnections[callId]
1261
+ connection?.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
1262
+ connection?.destroy()
1318
1263
  }
1319
1264
 
1320
- // Clear all collections
1265
+ // Clear all state
1321
1266
  activeCalls.clear()
1322
1267
  telecomConnections.clear()
1323
1268
  callMetadata.clear()
1324
- lockScreenBypassCallbacks.clear()
1325
- cachedEvents.clear()
1326
-
1327
- // Stop media
1328
- stopRingtone()
1329
- stopRingback()
1269
+ currentCallId = null
1330
1270
 
1331
1271
  // Release resources
1332
- wakeLock?.let {
1333
- if (it.isHeld) it.release()
1334
- }
1335
- wakeLock = null
1336
-
1337
- // Audio cleanup
1338
- abandonAudioFocus()
1339
- appContext?.let {
1340
- resetAudioMode(it)
1341
- unregisterAudioDeviceCallback()
1342
- }
1272
+ finalCleanup()
1343
1273
 
1344
- // Service cleanup
1345
- appContext?.let { stopForegroundService(it) }
1346
-
1347
- currentCallId = null
1348
- lockScreenBypassActive = false
1349
- hasAudioFocus = false
1350
- isSystemCallActive = false
1351
- isServiceRunning = false
1274
+ // Clear callbacks
1275
+ lockScreenBypassCallbacks.clear()
1276
+ eventHandler = null
1277
+ cachedEvents.clear()
1352
1278
 
1353
- Log.d(TAG, "CallEngine cleanup completed")
1279
+ // Reset initialization
1280
+ isInitialized.set(false)
1281
+ appContext = null
1354
1282
  }
1355
1283
  }