@qusaieilouti99/call-manager 0.1.48 → 0.1.49

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,18 +1,26 @@
1
1
  package com.margelo.nitro.qusaieilouti99.callmanager
2
2
 
3
- import android.app.*
3
+ import android.app.Activity
4
+ import android.app.Notification
5
+ import android.app.NotificationChannel
6
+ import android.app.NotificationManager
7
+ import android.app.PendingIntent
8
+ import android.app.Service
4
9
  import android.content.ComponentName
5
10
  import android.content.Context
6
11
  import android.content.Intent
12
+ import android.graphics.Color
7
13
  import android.media.AudioAttributes
8
14
  import android.media.AudioDeviceCallback
9
- import android.media.AudioManager
10
15
  import android.media.AudioDeviceInfo
16
+ import android.media.AudioManager
11
17
  import android.media.MediaPlayer
12
18
  import android.media.RingtoneManager
13
19
  import android.net.Uri
14
20
  import android.os.Build
15
21
  import android.os.Bundle
22
+ import android.os.Handler
23
+ import android.os.Looper
16
24
  import android.os.PowerManager
17
25
  import android.telecom.CallAudioState
18
26
  import android.telecom.Connection
@@ -22,13 +30,14 @@ import android.telecom.PhoneAccountHandle
22
30
  import android.telecom.TelecomManager
23
31
  import android.telecom.VideoProfile
24
32
  import android.util.Log
25
- import android.graphics.Color
26
- import android.app.Person
27
- import android.view.WindowManager
33
+ import kotlinx.coroutines.CoroutineScope
34
+ import kotlinx.coroutines.Dispatchers
35
+ import kotlinx.coroutines.launch
28
36
  import org.json.JSONArray
29
37
  import org.json.JSONObject
30
38
  import java.util.UUID
31
39
  import java.util.concurrent.ConcurrentHashMap
40
+ import java.util.concurrent.atomic.AtomicBoolean
32
41
 
33
42
  object CallEngine {
34
43
  private const val TAG = "CallEngine"
@@ -38,41 +47,55 @@ object CallEngine {
38
47
  private const val FOREGROUND_CHANNEL_ID = "call_foreground_channel"
39
48
  private const val FOREGROUND_NOTIF_ID = 1001
40
49
 
50
+ // Audio & Media
41
51
  private var ringtone: android.media.Ringtone? = null
42
52
  private var ringbackPlayer: MediaPlayer? = null
43
53
  private var audioManager: AudioManager? = null
44
54
  private var wakeLock: PowerManager.WakeLock? = null
45
55
  private var appContext: Context? = null
46
56
 
47
- // --- Multi-call state ---
57
+ // Call State Management
48
58
  private val activeCalls = ConcurrentHashMap<String, CallInfo>()
49
59
  private val telecomConnections = ConcurrentHashMap<String, Connection>()
50
60
  private var currentCallId: String? = null
51
- private var canMakeMultipleCalls: Boolean = true
61
+ private var canMakeMultipleCalls: Boolean = false
62
+
63
+ // Audio State Tracking
64
+ private var lastAudioRoutesInfo: AudioRoutesInfo? = null
65
+ private var lastMuteState: Boolean = false
52
66
 
53
- // --- Lock Screen Bypass State Management ---
67
+ // Lock Screen Bypass
54
68
  private var lockScreenBypassActive = false
55
69
  private val lockScreenBypassCallbacks = mutableSetOf<LockScreenBypassCallback>()
56
70
 
57
- interface LockScreenBypassCallback {
58
- fun onLockScreenBypassChanged(shouldBypass: Boolean)
59
- }
71
+ // Event System
72
+ private var eventHandler: ((CallEventType, String) -> Unit)? = null
73
+ private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
74
+
75
+ // Operation State
76
+ private val operationInProgress = AtomicBoolean(false)
60
77
 
61
78
  data class CallInfo(
62
79
  val callId: String,
63
80
  val callData: String,
64
81
  var state: CallState,
65
- val callType: String = "Audio"
82
+ val callType: String = "Audio",
83
+ val timestamp: Long = System.currentTimeMillis()
66
84
  )
67
85
 
68
86
  enum class CallState {
69
87
  INCOMING, DIALING, ACTIVE, HELD, ENDED
70
88
  }
71
89
 
72
- // --- Event handler for JS ---
73
- private var eventHandler: ((CallEventType, String) -> Unit)? = null
74
- private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
90
+ interface LockScreenBypassCallback {
91
+ fun onLockScreenBypassChanged(shouldBypass: Boolean)
92
+ }
93
+
94
+ interface ServerCallRejectCallback {
95
+ fun onRejectCall(callId: String, reason: String)
96
+ }
75
97
 
98
+ // --- Event System ---
76
99
  fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
77
100
  Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
78
101
  eventHandler = handler
@@ -85,7 +108,7 @@ object CallEngine {
85
108
  }
86
109
  }
87
110
 
88
- fun emitEvent(type: CallEventType, data: JSONObject) {
111
+ private fun emitEvent(type: CallEventType, data: JSONObject) {
89
112
  Log.d(TAG, "Emitting event: $type, data: $data")
90
113
  val dataString = data.toString()
91
114
  if (eventHandler != null) {
@@ -96,9 +119,7 @@ object CallEngine {
96
119
  }
97
120
  }
98
121
 
99
- fun getAppContext(): Context? = appContext
100
-
101
- // --- Lock Screen Bypass Management (Single Source of Truth) ---
122
+ // --- Lock Screen Bypass Management ---
102
123
  fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
103
124
  lockScreenBypassCallbacks.add(callback)
104
125
  }
@@ -112,8 +133,6 @@ object CallEngine {
112
133
  if (lockScreenBypassActive != shouldBypass) {
113
134
  lockScreenBypassActive = shouldBypass
114
135
  Log.d(TAG, "Lock screen bypass state changed: $lockScreenBypassActive")
115
-
116
- // Notify all registered callbacks
117
136
  lockScreenBypassCallbacks.forEach { callback ->
118
137
  try {
119
138
  callback.onLockScreenBypassChanged(shouldBypass)
@@ -133,13 +152,14 @@ object CallEngine {
133
152
  }
134
153
 
135
154
  fun removeTelecomConnection(callId: String) {
136
- telecomConnections.remove(callId)
137
- Log.d(TAG, "Removed Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
155
+ telecomConnections.remove(callId)?.let {
156
+ Log.d(TAG, "Removed Telecom Connection for callId: $callId. Total: ${telecomConnections.size}")
157
+ }
138
158
  }
139
159
 
140
- fun getTelecomConnection(callId: String): Connection? {
141
- return telecomConnections[callId]
142
- }
160
+ fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
161
+
162
+ fun getAppContext(): Context? = appContext
143
163
 
144
164
  // --- Public API ---
145
165
  fun setCanMakeMultipleCalls(allow: Boolean) {
@@ -163,20 +183,31 @@ object CallEngine {
163
183
  return result
164
184
  }
165
185
 
186
+ // --- Incoming Call Management ---
166
187
  fun reportIncomingCall(context: Context, callId: String, callData: String) {
167
188
  appContext = context.applicationContext
168
189
  Log.d(TAG, "reportIncomingCall: $callId, $callData")
169
190
 
170
- val callerName = try {
171
- val json = JSONObject(callData)
172
- json.optString("name", "Unknown")
173
- } catch (e: Exception) { "Unknown" }
191
+ // Check for call collision - reject second incoming call automatically
192
+ val incomingCall = activeCalls.values.find { it.state == CallState.INCOMING }
193
+ if (incomingCall != null && incomingCall.callId != callId) {
194
+ Log.d(TAG, "Incoming call collision detected. Auto-rejecting new call: $callId")
195
+
196
+ // Auto-reject the new call
197
+ rejectIncomingCallCollision(callId, "Another call is already incoming")
198
+ return
199
+ }
174
200
 
175
- val parsedCallType = try {
176
- val json = JSONObject(callData)
177
- json.optString("callType", "Audio")
178
- } catch (e: Exception) { "Audio" }
201
+ // Check if there's an active call when receiving incoming
202
+ val activeCall = activeCalls.values.find { it.state == CallState.ACTIVE }
203
+ if (activeCall != null) {
204
+ Log.d(TAG, "Active call exists when receiving incoming call. Auto-rejecting: $callId")
205
+ rejectIncomingCallCollision(callId, "Another call is already active")
206
+ return
207
+ }
179
208
 
209
+ val callerName = extractCallerName(callData)
210
+ val parsedCallType = extractCallType(callData)
180
211
  val isVideoCallBoolean = parsedCallType == "Video"
181
212
 
182
213
  if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
@@ -190,12 +221,14 @@ object CallEngine {
190
221
 
191
222
  showIncomingCallUI(context, callId, callerName, parsedCallType)
192
223
  registerPhoneAccount(context)
224
+
193
225
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
194
226
  val phoneAccountHandle = getPhoneAccountHandle(context)
195
227
  val extras = Bundle().apply {
196
228
  putString(MyConnectionService.EXTRA_CALL_DATA, callData)
197
229
  putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, isVideoCallBoolean)
198
230
  }
231
+
199
232
  try {
200
233
  telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
201
234
  startForegroundService(context)
@@ -207,24 +240,28 @@ object CallEngine {
207
240
  Log.e(TAG, "Failed to report incoming call: ${e.message}", e)
208
241
  endCall(context, callId)
209
242
  }
243
+
210
244
  updateLockScreenBypass()
211
- notifyCallStateChanged(context)
245
+ notifySpecificCallStateChanged(context, callId, CallState.INCOMING)
212
246
  }
213
247
 
248
+ // --- Outgoing Call Management ---
214
249
  fun startOutgoingCall(context: Context, callId: String, callData: String) {
215
250
  appContext = context.applicationContext
216
251
  Log.d(TAG, "startOutgoingCall: $callId, $callData")
217
252
 
218
- val targetName = try {
219
- val json = JSONObject(callData)
220
- json.optString("name", "Unknown")
221
- } catch (e: Exception) { "Unknown" }
222
-
223
- val parsedCallType = try {
224
- val json = JSONObject(callData)
225
- json.optString("callType", "Audio")
226
- } catch (e: Exception) { "Audio" }
253
+ // Validate outgoing call request
254
+ if (!validateOutgoingCallRequest()) {
255
+ Log.w(TAG, "Rejecting outgoing call - incoming/active call exists")
256
+ emitEvent(CallEventType.CALL_REJECTED, JSONObject().apply {
257
+ put("callId", callId)
258
+ put("reason", "Cannot start outgoing call while incoming or active call exists")
259
+ })
260
+ return
261
+ }
227
262
 
263
+ val targetName = extractCallerName(callData)
264
+ val parsedCallType = extractCallType(callData)
228
265
  val isVideoCallBoolean = parsedCallType == "Video"
229
266
 
230
267
  if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
@@ -239,7 +276,6 @@ object CallEngine {
239
276
  registerPhoneAccount(context)
240
277
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
241
278
  val phoneAccountHandle = getPhoneAccountHandle(context)
242
-
243
279
  val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, callId, null)
244
280
 
245
281
  val extras = Bundle().apply {
@@ -253,14 +289,11 @@ object CallEngine {
253
289
  telecomManager.placeCall(addressUri, extras)
254
290
  startForegroundService(context)
255
291
  Log.d(TAG, "Successfully reported outgoing call to TelecomManager via placeCall for $callId")
292
+
256
293
  startRingback()
257
294
  bringAppToForeground(context)
258
295
  keepScreenAwake(context, true)
259
- if (parsedCallType == "Video") {
260
- setAudioRoute(context, "Speaker")
261
- } else {
262
- setAudioRoute(context, "Earpiece")
263
- }
296
+ setInitialAudioRoute(context, parsedCallType)
264
297
  } catch (e: SecurityException) {
265
298
  Log.e(TAG, "SecurityException: Failed to start outgoing call via placeCall. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
266
299
  endCall(context, callId)
@@ -268,26 +301,31 @@ object CallEngine {
268
301
  Log.e(TAG, "Failed to start outgoing call via placeCall: ${e.message}", e)
269
302
  endCall(context, callId)
270
303
  }
304
+
271
305
  updateLockScreenBypass()
272
- notifyCallStateChanged(context)
306
+ notifySpecificCallStateChanged(context, callId, CallState.DIALING)
273
307
  }
274
308
 
275
- // SINGLE SOURCE OF TRUTH: Core function for handling when remote party answers
309
+ // --- Call Answer Management ---
276
310
  fun callAnsweredFromJS(context: Context, callId: String) {
277
311
  Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
278
312
  coreCallAnswered(context, callId, isLocalAnswer = false)
279
313
  }
280
314
 
281
- // SINGLE SOURCE OF TRUTH: Core function for handling local answer (user answering)
282
315
  fun answerCall(context: Context, callId: String) {
283
316
  Log.d(TAG, "answerCall: $callId - local party answering")
284
317
  coreCallAnswered(context, callId, isLocalAnswer = true)
285
318
  }
286
319
 
287
- // SINGLE SOURCE OF TRUTH: Core function that handles ALL call answering logic
288
320
  private fun coreCallAnswered(context: Context, callId: String, isLocalAnswer: Boolean) {
289
321
  Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
290
322
 
323
+ val callInfo = activeCalls[callId]
324
+ if (callInfo == null) {
325
+ Log.w(TAG, "Cannot answer call $callId - not found in active calls")
326
+ return
327
+ }
328
+
291
329
  // Stop all ringtones and notifications immediately
292
330
  stopRingtone()
293
331
  stopRingback()
@@ -301,7 +339,7 @@ object CallEngine {
301
339
  activeCalls.filter { it.key != callId }.values.forEach { it.state = CallState.HELD }
302
340
  }
303
341
 
304
- // Bring app to foreground only when call is answered
342
+ // Bring app to foreground when call is answered
305
343
  bringAppToForeground(context)
306
344
  startForegroundService(context)
307
345
  keepScreenAwake(context, true)
@@ -309,53 +347,86 @@ object CallEngine {
309
347
 
310
348
  updateLockScreenBypass()
311
349
 
312
- // Emit event
313
- emitEvent(CallEventType.CALL_ANSWERED, JSONObject().put("callId", callId))
314
- notifyCallStateChanged(context)
350
+ // Emit event with full call data instead of just callId
351
+ emitEvent(CallEventType.CALL_ANSWERED, JSONObject().apply {
352
+ put("callId", callId)
353
+ put("callData", callInfo.callData)
354
+ put("callType", callInfo.callType)
355
+ })
315
356
 
357
+ notifySpecificCallStateChanged(context, callId, CallState.ACTIVE)
316
358
  Log.d(TAG, "Call $callId successfully answered and UI cleaned up")
317
359
  }
318
360
 
361
+ // --- Call Control Methods ---
319
362
  fun holdCall(context: Context, callId: String) {
320
363
  Log.d(TAG, "holdCall: $callId")
364
+ val callInfo = activeCalls[callId]
365
+ if (callInfo?.state != CallState.ACTIVE) {
366
+ Log.w(TAG, "Cannot hold call $callId - not in active state")
367
+ return
368
+ }
369
+
321
370
  activeCalls[callId]?.state = CallState.HELD
322
371
  val connection = telecomConnections[callId]
323
372
  connection?.setOnHold()
373
+
374
+ updateForegroundNotification(context)
324
375
  emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
325
376
  updateLockScreenBypass()
326
- notifyCallStateChanged(context)
377
+ notifySpecificCallStateChanged(context, callId, CallState.HELD)
327
378
  }
328
379
 
329
380
  fun unholdCall(context: Context, callId: String) {
330
381
  Log.d(TAG, "unholdCall: $callId")
382
+ val callInfo = activeCalls[callId]
383
+ if (callInfo?.state != CallState.HELD) {
384
+ Log.w(TAG, "Cannot unhold call $callId - not in held state")
385
+ return
386
+ }
387
+
331
388
  activeCalls[callId]?.state = CallState.ACTIVE
332
389
  val connection = telecomConnections[callId]
333
390
  connection?.setActive()
391
+
392
+ updateForegroundNotification(context)
334
393
  emitEvent(CallEventType.CALL_UNHELD, JSONObject().put("callId", callId))
335
394
  updateLockScreenBypass()
336
- notifyCallStateChanged(context)
395
+ notifySpecificCallStateChanged(context, callId, CallState.ACTIVE)
337
396
  }
338
397
 
339
398
  fun muteCall(context: Context, callId: String) {
340
399
  Log.d(TAG, "muteCall: $callId")
341
400
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
401
+
402
+ // Only emit event if mute state actually changes
403
+ val wasMuted = audioManager?.isMicrophoneMute ?: false
342
404
  audioManager?.isMicrophoneMute = true
343
- emitEvent(CallEventType.CALL_MUTED, JSONObject().put("callId", callId))
405
+
406
+ if (!wasMuted) {
407
+ lastMuteState = true
408
+ emitEvent(CallEventType.CALL_MUTED, JSONObject().put("callId", callId))
409
+ }
344
410
  }
345
411
 
346
412
  fun unmuteCall(context: Context, callId: String) {
347
413
  Log.d(TAG, "unmuteCall: $callId")
348
414
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
415
+
416
+ // Only emit event if mute state actually changes
417
+ val wasMuted = audioManager?.isMicrophoneMute ?: false
349
418
  audioManager?.isMicrophoneMute = false
350
- emitEvent(CallEventType.CALL_UNMUTED, JSONObject().put("callId", callId))
419
+
420
+ if (wasMuted) {
421
+ lastMuteState = false
422
+ emitEvent(CallEventType.CALL_UNMUTED, JSONObject().put("callId", callId))
423
+ }
351
424
  }
352
425
 
353
- // SINGLE SOURCE OF TRUTH: Core function that handles ALL call ending logic
426
+ // --- Call End Management ---
354
427
  fun endCall(context: Context, callId: String) {
355
428
  appContext = context.applicationContext
356
429
  Log.d(TAG, "endCall: $callId")
357
-
358
- // Core cleanup logic
359
430
  coreEndCall(context, callId)
360
431
  }
361
432
 
@@ -365,23 +436,27 @@ object CallEngine {
365
436
  Log.d(TAG, "No active calls, nothing to do.")
366
437
  return
367
438
  }
439
+
368
440
  activeCalls.keys.toList().forEach { callId ->
369
441
  coreEndCall(context, callId)
370
442
  }
443
+
371
444
  activeCalls.clear()
372
445
  telecomConnections.clear()
373
446
  currentCallId = null
374
447
 
375
- // Final cleanup
376
448
  finalCleanup(context)
377
449
  updateLockScreenBypass()
378
- notifyCallStateChanged(context)
379
450
  }
380
451
 
381
- // SINGLE SOURCE OF TRUTH: Core function that handles ending a single call
382
452
  private fun coreEndCall(context: Context, callId: String) {
383
453
  Log.d(TAG, "coreEndCall: $callId")
384
454
 
455
+ val callInfo = activeCalls[callId] ?: run {
456
+ Log.w(TAG, "Call $callId not found in active calls")
457
+ return
458
+ }
459
+
385
460
  // Update call state
386
461
  activeCalls[callId]?.state = CallState.ENDED
387
462
  activeCalls.remove(callId)
@@ -410,144 +485,24 @@ object CallEngine {
410
485
  // If no more calls, do final cleanup
411
486
  if (activeCalls.isEmpty()) {
412
487
  finalCleanup(context)
488
+ } else {
489
+ updateForegroundNotification(context)
413
490
  }
414
491
 
415
492
  updateLockScreenBypass()
416
493
  emitEvent(CallEventType.CALL_ENDED, JSONObject().put("callId", callId))
417
- notifyCallStateChanged(context)
418
- }
419
-
420
- // SINGLE SOURCE OF TRUTH: Final cleanup when all calls are ended
421
- private fun finalCleanup(context: Context) {
422
- Log.d(TAG, "Performing final cleanup - no active calls remaining")
423
-
424
- stopForegroundService(context)
425
- keepScreenAwake(context, false)
426
- resetAudioMode(context)
427
- }
428
-
429
- fun getActiveCalls(): List<CallInfo> = activeCalls.values.toList()
430
- fun getCurrentCallId(): String? = currentCallId
431
- fun isCallActive(): Boolean = activeCalls.any {
432
- it.value.state == CallState.ACTIVE ||
433
- it.value.state == CallState.INCOMING ||
434
- it.value.state == CallState.DIALING
494
+ notifySpecificCallStateChanged(context, callId, CallState.ENDED)
435
495
  }
436
496
 
437
- // Enhanced incoming call UI with better cleanup
438
- fun showIncomingCallUI(context: Context, callId: String, callerName: String, callType: String) {
439
- Log.d(TAG, "Showing incoming call UI for $callId, caller: $callerName, callType: $callType")
440
- createNotificationChannel(context)
441
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
442
-
443
- val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
444
- action = "com.qusaieilouti99.callmanager.ANSWER_CALL"
445
- putExtra("callId", callId)
446
- }
447
- val answerPendingIntent = PendingIntent.getBroadcast(context, 0, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
448
-
449
- val declineIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
450
- action = "com.qusaieilouti99.callmanager.DECLINE_CALL"
451
- putExtra("callId", callId)
452
- }
453
- val declinePendingIntent = PendingIntent.getBroadcast(context, 1, declineIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
454
-
455
- val fullScreenIntent = Intent(context, CallActivity::class.java).apply {
456
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
457
- putExtra("callId", callId)
458
- putExtra("callerName", callerName)
459
- putExtra("callType", callType)
460
- }
461
- val fullScreenPendingIntent = PendingIntent.getActivity(context, 2, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
462
-
463
- val notification: Notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
464
- val person = Person.Builder().setName(callerName).setImportant(true).build()
465
- Notification.Builder(context, NOTIF_CHANNEL_ID)
466
- .setSmallIcon(android.R.drawable.sym_call_incoming)
467
- .setStyle(Notification.CallStyle.forIncomingCall(person, declinePendingIntent, answerPendingIntent))
468
- .setFullScreenIntent(fullScreenPendingIntent, true)
469
- .setOngoing(true)
470
- .setAutoCancel(false)
471
- .build()
472
- } else {
473
- Notification.Builder(context, NOTIF_CHANNEL_ID)
474
- .setSmallIcon(android.R.drawable.sym_call_incoming)
475
- .setContentTitle("Incoming Call")
476
- .setContentText(callerName)
477
- .setPriority(Notification.PRIORITY_HIGH)
478
- .setCategory(Notification.CATEGORY_CALL)
479
- .setFullScreenIntent(fullScreenPendingIntent, true)
480
- .addAction(android.R.drawable.sym_action_call, "Answer", answerPendingIntent)
481
- .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Decline", declinePendingIntent)
482
- .setOngoing(true)
483
- .setAutoCancel(false)
484
- .build()
485
- }
486
-
487
- notificationManager.notify(NOTIF_ID, notification)
488
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) playRingtone(context)
489
- if (callType == "Video") {
490
- setAudioRoute(context, "Speaker")
491
- }
492
- }
493
-
494
- fun cancelIncomingCallUI(context: Context) {
495
- Log.d(TAG, "Cancelling incoming call UI.")
496
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
497
- notificationManager.cancel(NOTIF_ID)
498
- stopRingtone()
499
- }
500
-
501
- fun startForegroundService(context: Context) {
502
- Log.d(TAG, "Starting CallForegroundService.")
503
- val intent = Intent(context, CallForegroundService::class.java)
504
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent)
505
- else context.startService(intent)
506
- }
507
-
508
- fun stopForegroundService(context: Context) {
509
- Log.d(TAG, "Stopping CallForegroundService.")
510
- val intent = Intent(context, CallForegroundService::class.java)
511
- context.stopService(intent)
512
- }
513
-
514
- // FIXED: Corrected bringAppToForeground method
515
- fun bringAppToForeground(context: Context) {
516
- val packageName = context.packageName
517
- val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
518
- launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
519
-
520
- // Handle lock screen bypass for active calls
521
- if (isCallActive()) {
522
- launchIntent?.putExtra("BYPASS_LOCK_SCREEN", true)
523
- launchIntent?.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
524
- Log.d(TAG, "App brought to foreground with lock screen bypass request for active call")
525
- } else {
526
- launchIntent?.removeExtra("BYPASS_LOCK_SCREEN")
527
- Log.d(TAG, "App brought to foreground without lock screen bypass")
528
- }
529
-
530
- try {
531
- context.startActivity(launchIntent)
532
-
533
- // Small delay to ensure activity is created before updating bypass
534
- android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
535
- updateLockScreenBypass()
536
- }, 100)
537
-
538
- } catch (e: Exception) {
539
- Log.e(TAG, "Failed to bring app to foreground: ${e.message}")
540
- }
541
- }
542
-
543
- // --- Audio Device Management ---
497
+ // --- Audio Management ---
544
498
  fun getAudioDevices(): AudioRoutesInfo {
545
499
  audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
546
500
  Log.e(TAG, "getAudioDevices: AudioManager is null or appContext is not set. Returning default.")
547
501
  return AudioRoutesInfo(emptyArray(), "Unknown")
548
502
  }
549
- val devices = mutableListOf<String>()
550
- var currentRoute: String = "Unknown"
503
+
504
+ val devices = mutableSetOf<String>()
505
+ var currentRoute = "Earpiece" // Default
551
506
 
552
507
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
553
508
  val audioDeviceInfoList = audioManager?.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
@@ -555,47 +510,31 @@ object CallEngine {
555
510
  when (device.type) {
556
511
  AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> {
557
512
  devices.add("Bluetooth")
558
- if (audioManager?.isBluetoothScoOn == true && !device.isSource) currentRoute = "Bluetooth"
559
513
  }
560
514
  AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET -> {
561
515
  devices.add("Headset")
562
- if (audioManager?.isWiredHeadsetOn == true && !device.isSource) currentRoute = "Headset"
563
516
  }
564
517
  AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> {
565
518
  devices.add("Speaker")
566
- if (audioManager?.isSpeakerphoneOn == true && !device.isSource) currentRoute = "Speaker"
567
519
  }
568
520
  AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> {
569
521
  devices.add("Earpiece")
570
- if (audioManager?.isSpeakerphoneOn == false && audioManager?.isWiredHeadsetOn == false && audioManager?.isBluetoothScoOn == false && !device.isSource) {
571
- currentRoute = "Earpiece"
572
- }
573
522
  }
574
- else -> Log.d(TAG, "Unknown audio device type: ${device.type}")
575
523
  }
576
524
  }
577
525
  } else {
578
- devices.add("Speaker")
579
- devices.add("Earpiece")
580
- if (audioManager?.isSpeakerphoneOn == true) currentRoute = "Speaker"
581
- else currentRoute = "Earpiece"
526
+ devices.addAll(listOf("Speaker", "Earpiece"))
582
527
  }
583
528
 
584
- val distinctDevices = devices.distinct().toTypedArray()
585
-
586
- if (currentRoute == "Unknown" || currentRoute == "Earpiece") {
587
- if (audioManager?.isBluetoothScoOn == true) {
588
- currentRoute = "Bluetooth"
589
- } else if (audioManager?.isSpeakerphoneOn == true) {
590
- currentRoute = "Speaker"
591
- } else if (audioManager?.isWiredHeadsetOn == true) {
592
- currentRoute = "Headset"
593
- } else {
594
- currentRoute = "Earpiece"
595
- }
529
+ // Determine current route
530
+ currentRoute = when {
531
+ audioManager?.isBluetoothScoOn == true -> "Bluetooth"
532
+ audioManager?.isSpeakerphoneOn == true -> "Speaker"
533
+ audioManager?.isWiredHeadsetOn == true -> "Headset"
534
+ else -> "Earpiece"
596
535
  }
597
536
 
598
- val result = AudioRoutesInfo(distinctDevices, currentRoute)
537
+ val result = AudioRoutesInfo(devices.toTypedArray(), currentRoute)
599
538
  Log.d(TAG, "Audio devices info: $result")
600
539
  return result
601
540
  }
@@ -604,6 +543,9 @@ object CallEngine {
604
543
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
605
544
  Log.d(TAG, "Attempting to set audio route to: $route. Current mode: ${audioManager?.mode}")
606
545
 
546
+ val previousRoute = getCurrentAudioRoute()
547
+
548
+ // Reset all routes first
607
549
  audioManager?.isSpeakerphoneOn = false
608
550
  audioManager?.stopBluetoothSco()
609
551
  audioManager?.isBluetoothScoOn = false
@@ -616,7 +558,6 @@ object CallEngine {
616
558
  }
617
559
  "Earpiece" -> {
618
560
  Log.d(TAG, "Setting audio route to Earpiece.")
619
- audioManager?.isSpeakerphoneOn = false
620
561
  audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
621
562
  }
622
563
  "Bluetooth" -> {
@@ -631,9 +572,43 @@ object CallEngine {
631
572
  }
632
573
  else -> {
633
574
  Log.w(TAG, "Unknown audio route: $route. No action taken.")
575
+ return
634
576
  }
635
577
  }
636
- emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, JSONObject().put("route", route))
578
+
579
+ // Only emit event if route actually changed
580
+ val newRoute = getCurrentAudioRoute()
581
+ if (previousRoute != newRoute) {
582
+ emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, JSONObject().put("route", newRoute))
583
+ }
584
+ }
585
+
586
+ private fun getCurrentAudioRoute(): String {
587
+ return when {
588
+ audioManager?.isBluetoothScoOn == true -> "Bluetooth"
589
+ audioManager?.isSpeakerphoneOn == true -> "Speaker"
590
+ audioManager?.isWiredHeadsetOn == true -> "Headset"
591
+ else -> "Earpiece"
592
+ }
593
+ }
594
+
595
+ private fun setInitialAudioRoute(context: Context, callType: String) {
596
+ // Get available audio devices to determine priority
597
+ val availableDevices = getAudioDevices()
598
+
599
+ val defaultRoute = when {
600
+ // Prioritize Bluetooth if available (latest connected device)
601
+ availableDevices.devices.contains("Bluetooth") -> "Bluetooth"
602
+ // Then wired headset
603
+ availableDevices.devices.contains("Headset") -> "Headset"
604
+ // For video calls, default to speaker if no priority device
605
+ callType == "Video" -> "Speaker"
606
+ // For audio calls, default to earpiece if no priority device
607
+ else -> "Earpiece"
608
+ }
609
+
610
+ Log.d(TAG, "Setting initial audio route for $callType call: $defaultRoute")
611
+ setAudioRoute(context, defaultRoute)
637
612
  }
638
613
 
639
614
  fun resetAudioMode(context: Context) {
@@ -649,7 +624,51 @@ object CallEngine {
649
624
  }
650
625
  }
651
626
 
652
- // --- Screen Awake Management ---
627
+ // --- Audio Device Callback ---
628
+ private val audioDeviceCallback = object : AudioDeviceCallback() {
629
+ override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
630
+ Log.d(TAG, "Audio devices added. Checking for changes.")
631
+ emitAudioDevicesChangedIfNeeded()
632
+ }
633
+
634
+ override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
635
+ Log.d(TAG, "Audio devices removed. Checking for changes.")
636
+ emitAudioDevicesChangedIfNeeded()
637
+ }
638
+ }
639
+
640
+ fun registerAudioDeviceCallback(context: Context) {
641
+ appContext = context.applicationContext
642
+ audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
643
+ audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
644
+ Log.d(TAG, "Audio device callback registered.")
645
+ }
646
+
647
+ fun unregisterAudioDeviceCallback(context: Context) {
648
+ audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
649
+ audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
650
+ Log.d(TAG, "Audio device callback unregistered.")
651
+ }
652
+
653
+ private fun emitAudioDevicesChangedIfNeeded() {
654
+ val context = appContext ?: return
655
+ val currentAudioInfo = getAudioDevices()
656
+
657
+ // Only emit if something actually changed
658
+ if (lastAudioRoutesInfo == null ||
659
+ !currentAudioInfo.devices.contentEquals(lastAudioRoutesInfo!!.devices) ||
660
+ currentAudioInfo.currentRoute != lastAudioRoutesInfo!!.currentRoute) {
661
+
662
+ lastAudioRoutesInfo = currentAudioInfo
663
+ val jsonPayload = JSONObject().apply {
664
+ put("devices", JSONArray(currentAudioInfo.devices.toList()))
665
+ put("currentRoute", currentAudioInfo.currentRoute)
666
+ }
667
+ emitEvent(CallEventType.AUDIO_DEVICES_CHANGED, jsonPayload)
668
+ }
669
+ }
670
+
671
+ // --- Screen Management ---
653
672
  fun keepScreenAwake(context: Context, keepAwake: Boolean) {
654
673
  val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
655
674
  if (keepAwake) {
@@ -660,8 +679,6 @@ object CallEngine {
660
679
  )
661
680
  wakeLock?.acquire(10 * 60 * 1000L /* 10 minutes */)
662
681
  Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK.")
663
- } else {
664
- Log.d(TAG, "Wake lock already held.")
665
682
  }
666
683
  } else {
667
684
  wakeLock?.let {
@@ -674,61 +691,58 @@ object CallEngine {
674
691
  }
675
692
  }
676
693
 
677
- // --- Audio Device Change Listener ---
678
- private val audioDeviceCallback = object : AudioDeviceCallback() {
679
- override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
680
- Log.d(TAG, "Audio devices added. Emitting AUDIO_DEVICES_CHANGED.")
681
- emitAudioDevicesChanged()
682
- }
683
- override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
684
- Log.d(TAG, "Audio devices removed. Emitting AUDIO_DEVICES_CHANGED.")
685
- emitAudioDevicesChanged()
686
- }
694
+ // --- Utility Methods ---
695
+ fun getActiveCalls(): List<CallInfo> = activeCalls.values.toList()
696
+ fun getCurrentCallId(): String? = currentCallId
697
+ fun isCallActive(): Boolean = activeCalls.any {
698
+ it.value.state == CallState.ACTIVE ||
699
+ it.value.state == CallState.INCOMING ||
700
+ it.value.state == CallState.DIALING
687
701
  }
688
702
 
689
- fun registerAudioDeviceCallback(context: Context) {
690
- appContext = context.applicationContext
691
- audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
692
- audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
693
- Log.d(TAG, "Audio device callback registered.")
703
+ private fun validateOutgoingCallRequest(): Boolean {
704
+ return !activeCalls.any {
705
+ it.value.state == CallState.INCOMING || it.value.state == CallState.ACTIVE
706
+ }
694
707
  }
695
708
 
696
- fun unregisterAudioDeviceCallback(context: Context) {
697
- audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
698
- audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
699
- Log.d(TAG, "Audio device callback unregistered.")
709
+ private fun extractCallerName(callData: String): String {
710
+ return try {
711
+ JSONObject(callData).optString("name", "Unknown")
712
+ } catch (e: Exception) {
713
+ "Unknown"
714
+ }
700
715
  }
701
716
 
702
- fun emitAudioDevicesChanged() {
703
- val context = appContext ?: run {
704
- Log.w(TAG, "Cannot emit AudioDevicesChanged: appContext is null.")
705
- return
706
- }
707
- val audioInfo = getAudioDevices()
708
- val jsonPayload = JSONObject().apply {
709
- put("devices", JSONArray(audioInfo.devices.toList()))
710
- put("currentRoute", audioInfo.currentRoute)
717
+ private fun extractCallType(callData: String): String {
718
+ return try {
719
+ JSONObject(callData).optString("callType", "Audio")
720
+ } catch (e: Exception) {
721
+ "Audio"
711
722
  }
712
- emitEvent(CallEventType.AUDIO_DEVICES_CHANGED, jsonPayload)
713
723
  }
714
724
 
715
- // --- Call State Change Notification ---
716
- private fun notifyCallStateChanged(context: Context) {
717
- val calls = getActiveCalls()
718
- val jsonArray = JSONArray()
719
- calls.forEach {
720
- val obj = JSONObject()
721
- obj.put("callId", it.callId)
722
- obj.put("callData", it.callData)
723
- obj.put("state", it.state.name)
724
- obj.put("callType", it.callType)
725
- jsonArray.put(obj)
725
+ private fun rejectIncomingCallCollision(callId: String, reason: String) {
726
+ // Provide space for server HTTP request
727
+ CoroutineScope(Dispatchers.IO).launch {
728
+ try {
729
+ // TODO: Add your server HTTP request here
730
+ // Example:
731
+ // ApiService.rejectCall(callId, reason)
732
+ Log.d(TAG, "Server rejection request would be made here for callId: $callId, reason: $reason")
733
+ } catch (e: Exception) {
734
+ Log.e(TAG, "Failed to send rejection to server", e)
735
+ }
726
736
  }
727
- val jsonPayload = JSONObject().put("calls", jsonArray)
728
- Log.d(TAG, "Call state changed. Emitting CALL_STATE_CHANGED: $jsonPayload")
729
- emitEvent(CallEventType.CALL_STATE_CHANGED, jsonPayload)
737
+
738
+ // Emit rejection event
739
+ emitEvent(CallEventType.CALL_REJECTED, JSONObject().apply {
740
+ put("callId", callId)
741
+ put("reason", reason)
742
+ })
730
743
  }
731
744
 
745
+ // --- Notification Management ---
732
746
  private fun createNotificationChannel(context: Context) {
733
747
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
734
748
  val channel = NotificationChannel(
@@ -740,6 +754,7 @@ object CallEngine {
740
754
  channel.enableLights(true)
741
755
  channel.lightColor = Color.GREEN
742
756
  channel.enableVibration(true)
757
+
743
758
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
744
759
  channel.setSound(
745
760
  RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE),
@@ -752,20 +767,138 @@ object CallEngine {
752
767
  channel.setSound(null, null)
753
768
  channel.importance = NotificationManager.IMPORTANCE_HIGH
754
769
  }
770
+
755
771
  val manager = context.getSystemService(NotificationManager::class.java)
756
772
  manager.createNotificationChannel(channel)
757
773
  Log.d(TAG, "Notification channel '$NOTIF_CHANNEL_ID' created/updated.")
758
774
  }
759
775
  }
760
776
 
761
- // PhoneAccount registration is used for both INCOMING and OUTGOING calls when interacting with Telecom
777
+ fun showIncomingCallUI(context: Context, callId: String, callerName: String, callType: String) {
778
+ Log.d(TAG, "Showing incoming call UI for $callId, caller: $callerName, callType: $callType")
779
+ createNotificationChannel(context)
780
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
781
+
782
+ val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
783
+ action = "com.qusaieilouti99.callmanager.ANSWER_CALL"
784
+ putExtra("callId", callId)
785
+ }
786
+ val answerPendingIntent = PendingIntent.getBroadcast(
787
+ context, 0, answerIntent,
788
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
789
+ )
790
+
791
+ val declineIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
792
+ action = "com.qusaieilouti99.callmanager.DECLINE_CALL"
793
+ putExtra("callId", callId)
794
+ }
795
+ val declinePendingIntent = PendingIntent.getBroadcast(
796
+ context, 1, declineIntent,
797
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
798
+ )
799
+
800
+ val fullScreenIntent = Intent(context, CallActivity::class.java).apply {
801
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
802
+ putExtra("callId", callId)
803
+ putExtra("callerName", callerName)
804
+ putExtra("callType", callType)
805
+ }
806
+ val fullScreenPendingIntent = PendingIntent.getActivity(
807
+ context, 2, fullScreenIntent,
808
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
809
+ )
810
+
811
+ val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
812
+ val person = android.app.Person.Builder().setName(callerName).setImportant(true).build()
813
+ Notification.Builder(context, NOTIF_CHANNEL_ID)
814
+ .setSmallIcon(android.R.drawable.sym_call_incoming)
815
+ .setStyle(Notification.CallStyle.forIncomingCall(person, declinePendingIntent, answerPendingIntent))
816
+ .setFullScreenIntent(fullScreenPendingIntent, true)
817
+ .setOngoing(true)
818
+ .setAutoCancel(false)
819
+ .build()
820
+ } else {
821
+ Notification.Builder(context, NOTIF_CHANNEL_ID)
822
+ .setSmallIcon(android.R.drawable.sym_call_incoming)
823
+ .setContentTitle("Incoming Call")
824
+ .setContentText(callerName)
825
+ .setPriority(Notification.PRIORITY_HIGH)
826
+ .setCategory(Notification.CATEGORY_CALL)
827
+ .setFullScreenIntent(fullScreenPendingIntent, true)
828
+ .addAction(android.R.drawable.sym_action_call, "Answer", answerPendingIntent)
829
+ .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Decline", declinePendingIntent)
830
+ .setOngoing(true)
831
+ .setAutoCancel(false)
832
+ .build()
833
+ }
834
+
835
+ notificationManager.notify(NOTIF_ID, notification)
836
+
837
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
838
+ playRingtone(context)
839
+ }
840
+
841
+ setInitialAudioRoute(context, callType)
842
+ }
843
+
844
+ fun cancelIncomingCallUI(context: Context) {
845
+ Log.d(TAG, "Cancelling incoming call UI.")
846
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
847
+ notificationManager.cancel(NOTIF_ID)
848
+ stopRingtone()
849
+ }
850
+
851
+ // --- Service Management ---
852
+ fun startForegroundService(context: Context) {
853
+ Log.d(TAG, "Starting CallForegroundService.")
854
+ val intent = Intent(context, CallForegroundService::class.java)
855
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
856
+ context.startForegroundService(intent)
857
+ } else {
858
+ context.startService(intent)
859
+ }
860
+ }
861
+
862
+ fun stopForegroundService(context: Context) {
863
+ Log.d(TAG, "Stopping CallForegroundService.")
864
+ val intent = Intent(context, CallForegroundService::class.java)
865
+ context.stopService(intent)
866
+ }
867
+
868
+ fun bringAppToForeground(context: Context) {
869
+ val packageName = context.packageName
870
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
871
+ launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
872
+
873
+ if (isCallActive()) {
874
+ launchIntent?.putExtra("BYPASS_LOCK_SCREEN", true)
875
+ launchIntent?.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
876
+ Log.d(TAG, "App brought to foreground with lock screen bypass request for active call")
877
+ } else {
878
+ launchIntent?.removeExtra("BYPASS_LOCK_SCREEN")
879
+ Log.d(TAG, "App brought to foreground without lock screen bypass")
880
+ }
881
+
882
+ try {
883
+ context.startActivity(launchIntent)
884
+ Handler(Looper.getMainLooper()).postDelayed({
885
+ updateLockScreenBypass()
886
+ }, 100)
887
+ } catch (e: Exception) {
888
+ Log.e(TAG, "Failed to bring app to foreground: ${e.message}")
889
+ }
890
+ }
891
+
892
+ // --- Phone Account Management ---
762
893
  private fun registerPhoneAccount(context: Context) {
763
894
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
764
895
  val phoneAccountHandle = getPhoneAccountHandle(context)
896
+
765
897
  if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
766
898
  val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
767
899
  .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
768
900
  .build()
901
+
769
902
  try {
770
903
  telecomManager.registerPhoneAccount(phoneAccount)
771
904
  Log.d(TAG, "PhoneAccount registered successfully.")
@@ -786,12 +919,13 @@ object CallEngine {
786
919
  )
787
920
  }
788
921
 
789
- // --- Ringtone Management (for incoming calls, pre-Android S) ---
922
+ // --- Media Management ---
790
923
  fun playRingtone(context: Context) {
791
924
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
792
925
  Log.d(TAG, "playRingtone: Android S+ detected, system will handle ringtone via Telecom.")
793
926
  return
794
927
  }
928
+
795
929
  try {
796
930
  Log.d(TAG, "Playing ringtone (for Android < S).")
797
931
  val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
@@ -808,7 +942,7 @@ object CallEngine {
808
942
 
809
943
  fun stopRingtone() {
810
944
  try {
811
- if (ringtone != null && ringtone!!.isPlaying) {
945
+ if (ringtone?.isPlaying == true) {
812
946
  ringtone?.stop()
813
947
  Log.d(TAG, "Ringtone stopped.")
814
948
  }
@@ -818,12 +952,12 @@ object CallEngine {
818
952
  ringtone = null
819
953
  }
820
954
 
821
- // --- Ringback Tone Management (for outgoing calls) ---
822
955
  private fun startRingback() {
823
- if (ringbackPlayer != null && ringbackPlayer!!.isPlaying) {
956
+ if (ringbackPlayer?.isPlaying == true) {
824
957
  Log.d(TAG, "Ringback tone already playing.")
825
958
  return
826
959
  }
960
+
827
961
  try {
828
962
  val ringbackUri = Uri.parse("android.resource://${appContext?.packageName}/raw/ringback_tone")
829
963
  ringbackPlayer = MediaPlayer.create(appContext, ringbackUri)
@@ -831,6 +965,7 @@ object CallEngine {
831
965
  Log.e(TAG, "Failed to create MediaPlayer for ringback. Check raw/ringback_tone.mp3 exists.")
832
966
  return
833
967
  }
968
+
834
969
  ringbackPlayer?.apply {
835
970
  isLooping = true
836
971
  setAudioAttributes(
@@ -849,7 +984,7 @@ object CallEngine {
849
984
 
850
985
  private fun stopRingback() {
851
986
  try {
852
- if (ringbackPlayer != null && ringbackPlayer!!.isPlaying) {
987
+ if (ringbackPlayer?.isPlaying == true) {
853
988
  ringbackPlayer?.stop()
854
989
  ringbackPlayer?.release()
855
990
  Log.d(TAG, "Ringback tone stopped and released.")
@@ -860,4 +995,46 @@ object CallEngine {
860
995
  ringbackPlayer = null
861
996
  }
862
997
  }
998
+
999
+ // --- Event Management ---
1000
+ private fun notifySpecificCallStateChanged(context: Context, callId: String, newState: CallState) {
1001
+ val callInfo = activeCalls[callId] ?: return
1002
+
1003
+ val jsonPayload = JSONObject().apply {
1004
+ put("callId", callId)
1005
+ put("callData", callInfo.callData)
1006
+ put("state", newState.name)
1007
+ put("callType", callInfo.callType)
1008
+ }
1009
+
1010
+ Log.d(TAG, "Specific call state changed. Emitting CALL_STATE_CHANGED for $callId: $newState")
1011
+ emitEvent(CallEventType.CALL_STATE_CHANGED, jsonPayload)
1012
+ }
1013
+
1014
+ private fun updateForegroundNotification(context: Context) {
1015
+ val activeCall = activeCalls.values.find { it.state == CallState.ACTIVE }
1016
+ val heldCall = activeCalls.values.find { it.state == CallState.HELD }
1017
+
1018
+ val callToShow = activeCall ?: heldCall
1019
+ callToShow?.let {
1020
+ val intent = Intent(context, CallForegroundService::class.java)
1021
+ intent.putExtra("UPDATE_NOTIFICATION", true)
1022
+ intent.putExtra("callId", it.callId)
1023
+ intent.putExtra("callData", it.callData)
1024
+ intent.putExtra("state", it.state.name)
1025
+
1026
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1027
+ context.startForegroundService(intent)
1028
+ } else {
1029
+ context.startService(intent)
1030
+ }
1031
+ }
1032
+ }
1033
+
1034
+ private fun finalCleanup(context: Context) {
1035
+ Log.d(TAG, "Performing final cleanup - no active calls remaining")
1036
+ stopForegroundService(context)
1037
+ keepScreenAwake(context, false)
1038
+ resetAudioMode(context)
1039
+ }
863
1040
  }