@qusaieilouti99/call-manager 0.1.65 → 0.1.67

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