@qusaieilouti99/call-manager 0.1.47 → 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,12 +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
33
+ import kotlinx.coroutines.CoroutineScope
34
+ import kotlinx.coroutines.Dispatchers
35
+ import kotlinx.coroutines.launch
27
36
  import org.json.JSONArray
28
37
  import org.json.JSONObject
29
38
  import java.util.UUID
30
39
  import java.util.concurrent.ConcurrentHashMap
40
+ import java.util.concurrent.atomic.AtomicBoolean
31
41
 
32
42
  object CallEngine {
33
43
  private const val TAG = "CallEngine"
@@ -37,33 +47,55 @@ object CallEngine {
37
47
  private const val FOREGROUND_CHANNEL_ID = "call_foreground_channel"
38
48
  private const val FOREGROUND_NOTIF_ID = 1001
39
49
 
50
+ // Audio & Media
40
51
  private var ringtone: android.media.Ringtone? = null
41
52
  private var ringbackPlayer: MediaPlayer? = null
42
53
  private var audioManager: AudioManager? = null
43
54
  private var wakeLock: PowerManager.WakeLock? = null
44
55
  private var appContext: Context? = null
45
56
 
46
- // --- Multi-call state ---
57
+ // Call State Management
47
58
  private val activeCalls = ConcurrentHashMap<String, CallInfo>()
48
59
  private val telecomConnections = ConcurrentHashMap<String, Connection>()
49
60
  private var currentCallId: String? = null
50
- 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
66
+
67
+ // Lock Screen Bypass
68
+ private var lockScreenBypassActive = false
69
+ private val lockScreenBypassCallbacks = mutableSetOf<LockScreenBypassCallback>()
70
+
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)
51
77
 
52
78
  data class CallInfo(
53
79
  val callId: String,
54
80
  val callData: String,
55
81
  var state: CallState,
56
- val callType: String = "Audio"
82
+ val callType: String = "Audio",
83
+ val timestamp: Long = System.currentTimeMillis()
57
84
  )
58
85
 
59
86
  enum class CallState {
60
87
  INCOMING, DIALING, ACTIVE, HELD, ENDED
61
88
  }
62
89
 
63
- // --- Event handler for JS ---
64
- private var eventHandler: ((CallEventType, String) -> Unit)? = null
65
- private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
90
+ interface LockScreenBypassCallback {
91
+ fun onLockScreenBypassChanged(shouldBypass: Boolean)
92
+ }
66
93
 
94
+ interface ServerCallRejectCallback {
95
+ fun onRejectCall(callId: String, reason: String)
96
+ }
97
+
98
+ // --- Event System ---
67
99
  fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
68
100
  Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
69
101
  eventHandler = handler
@@ -76,7 +108,7 @@ object CallEngine {
76
108
  }
77
109
  }
78
110
 
79
- fun emitEvent(type: CallEventType, data: JSONObject) {
111
+ private fun emitEvent(type: CallEventType, data: JSONObject) {
80
112
  Log.d(TAG, "Emitting event: $type, data: $data")
81
113
  val dataString = data.toString()
82
114
  if (eventHandler != null) {
@@ -87,7 +119,31 @@ object CallEngine {
87
119
  }
88
120
  }
89
121
 
90
- fun getAppContext(): Context? = appContext
122
+ // --- Lock Screen Bypass Management ---
123
+ fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
124
+ lockScreenBypassCallbacks.add(callback)
125
+ }
126
+
127
+ fun unregisterLockScreenBypassCallback(callback: LockScreenBypassCallback) {
128
+ lockScreenBypassCallbacks.remove(callback)
129
+ }
130
+
131
+ private fun updateLockScreenBypass() {
132
+ val shouldBypass = isCallActive()
133
+ if (lockScreenBypassActive != shouldBypass) {
134
+ lockScreenBypassActive = shouldBypass
135
+ Log.d(TAG, "Lock screen bypass state changed: $lockScreenBypassActive")
136
+ lockScreenBypassCallbacks.forEach { callback ->
137
+ try {
138
+ callback.onLockScreenBypassChanged(shouldBypass)
139
+ } catch (e: Exception) {
140
+ Log.w(TAG, "Error notifying lock screen bypass callback", e)
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ fun isLockScreenBypassActive(): Boolean = lockScreenBypassActive
91
147
 
92
148
  // --- Telecom Connection Management ---
93
149
  fun addTelecomConnection(callId: String, connection: Connection) {
@@ -96,13 +152,14 @@ object CallEngine {
96
152
  }
97
153
 
98
154
  fun removeTelecomConnection(callId: String) {
99
- telecomConnections.remove(callId)
100
- 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
+ }
101
158
  }
102
159
 
103
- fun getTelecomConnection(callId: String): Connection? {
104
- return telecomConnections[callId]
105
- }
160
+ fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
161
+
162
+ fun getAppContext(): Context? = appContext
106
163
 
107
164
  // --- Public API ---
108
165
  fun setCanMakeMultipleCalls(allow: Boolean) {
@@ -126,20 +183,31 @@ object CallEngine {
126
183
  return result
127
184
  }
128
185
 
186
+ // --- Incoming Call Management ---
129
187
  fun reportIncomingCall(context: Context, callId: String, callData: String) {
130
188
  appContext = context.applicationContext
131
189
  Log.d(TAG, "reportIncomingCall: $callId, $callData")
132
190
 
133
- val callerName = try {
134
- val json = JSONObject(callData)
135
- json.optString("name", "Unknown")
136
- } 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")
137
195
 
138
- val parsedCallType = try {
139
- val json = JSONObject(callData)
140
- json.optString("callType", "Audio")
141
- } catch (e: Exception) { "Audio" }
196
+ // Auto-reject the new call
197
+ rejectIncomingCallCollision(callId, "Another call is already incoming")
198
+ return
199
+ }
142
200
 
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
+ }
208
+
209
+ val callerName = extractCallerName(callData)
210
+ val parsedCallType = extractCallType(callData)
143
211
  val isVideoCallBoolean = parsedCallType == "Video"
144
212
 
145
213
  if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
@@ -153,17 +221,18 @@ object CallEngine {
153
221
 
154
222
  showIncomingCallUI(context, callId, callerName, parsedCallType)
155
223
  registerPhoneAccount(context)
224
+
156
225
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
157
226
  val phoneAccountHandle = getPhoneAccountHandle(context)
158
227
  val extras = Bundle().apply {
159
228
  putString(MyConnectionService.EXTRA_CALL_DATA, callData)
160
229
  putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, isVideoCallBoolean)
161
230
  }
231
+
162
232
  try {
163
233
  telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
164
234
  startForegroundService(context)
165
235
  Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
166
- // REMOVED: Don't bring app to foreground for incoming calls - only when answered
167
236
  } catch (e: SecurityException) {
168
237
  Log.e(TAG, "SecurityException: Failed to report incoming call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
169
238
  endCall(context, callId)
@@ -171,23 +240,28 @@ object CallEngine {
171
240
  Log.e(TAG, "Failed to report incoming call: ${e.message}", e)
172
241
  endCall(context, callId)
173
242
  }
174
- notifyCallStateChanged(context)
243
+
244
+ updateLockScreenBypass()
245
+ notifySpecificCallStateChanged(context, callId, CallState.INCOMING)
175
246
  }
176
247
 
248
+ // --- Outgoing Call Management ---
177
249
  fun startOutgoingCall(context: Context, callId: String, callData: String) {
178
250
  appContext = context.applicationContext
179
251
  Log.d(TAG, "startOutgoingCall: $callId, $callData")
180
252
 
181
- val targetName = try {
182
- val json = JSONObject(callData)
183
- json.optString("name", "Unknown")
184
- } catch (e: Exception) { "Unknown" }
185
-
186
- val parsedCallType = try {
187
- val json = JSONObject(callData)
188
- json.optString("callType", "Audio")
189
- } 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
+ }
190
262
 
263
+ val targetName = extractCallerName(callData)
264
+ val parsedCallType = extractCallType(callData)
191
265
  val isVideoCallBoolean = parsedCallType == "Video"
192
266
 
193
267
  if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
@@ -202,7 +276,6 @@ object CallEngine {
202
276
  registerPhoneAccount(context)
203
277
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
204
278
  val phoneAccountHandle = getPhoneAccountHandle(context)
205
-
206
279
  val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, callId, null)
207
280
 
208
281
  val extras = Bundle().apply {
@@ -216,15 +289,11 @@ object CallEngine {
216
289
  telecomManager.placeCall(addressUri, extras)
217
290
  startForegroundService(context)
218
291
  Log.d(TAG, "Successfully reported outgoing call to TelecomManager via placeCall for $callId")
292
+
219
293
  startRingback()
220
- // CHANGED: Bring app to foreground for outgoing calls
221
294
  bringAppToForeground(context)
222
295
  keepScreenAwake(context, true)
223
- if (parsedCallType == "Video") {
224
- setAudioRoute(context, "Speaker")
225
- } else {
226
- setAudioRoute(context, "Earpiece")
227
- }
296
+ setInitialAudioRoute(context, parsedCallType)
228
297
  } catch (e: SecurityException) {
229
298
  Log.e(TAG, "SecurityException: Failed to start outgoing call via placeCall. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
230
299
  endCall(context, callId)
@@ -232,25 +301,31 @@ object CallEngine {
232
301
  Log.e(TAG, "Failed to start outgoing call via placeCall: ${e.message}", e)
233
302
  endCall(context, callId)
234
303
  }
235
- notifyCallStateChanged(context)
304
+
305
+ updateLockScreenBypass()
306
+ notifySpecificCallStateChanged(context, callId, CallState.DIALING)
236
307
  }
237
308
 
238
- // SINGLE SOURCE OF TRUTH: Core function for handling when remote party answers
309
+ // --- Call Answer Management ---
239
310
  fun callAnsweredFromJS(context: Context, callId: String) {
240
311
  Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
241
312
  coreCallAnswered(context, callId, isLocalAnswer = false)
242
313
  }
243
314
 
244
- // SINGLE SOURCE OF TRUTH: Core function for handling local answer (user answering)
245
315
  fun answerCall(context: Context, callId: String) {
246
316
  Log.d(TAG, "answerCall: $callId - local party answering")
247
317
  coreCallAnswered(context, callId, isLocalAnswer = true)
248
318
  }
249
319
 
250
- // SINGLE SOURCE OF TRUTH: Core function that handles ALL call answering logic
251
320
  private fun coreCallAnswered(context: Context, callId: String, isLocalAnswer: Boolean) {
252
321
  Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
253
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
+
254
329
  // Stop all ringtones and notifications immediately
255
330
  stopRingtone()
256
331
  stopRingback()
@@ -264,79 +339,94 @@ object CallEngine {
264
339
  activeCalls.filter { it.key != callId }.values.forEach { it.state = CallState.HELD }
265
340
  }
266
341
 
267
- // Bring app to foreground only when call is answered
342
+ // Bring app to foreground when call is answered
268
343
  bringAppToForeground(context)
269
344
  startForegroundService(context)
270
345
  keepScreenAwake(context, true)
271
346
  resetAudioMode(context)
272
347
 
273
- // ADDED: Ensure MainActivity shows above lock screen during call
274
- updateMainActivityLockScreenBypass()
348
+ updateLockScreenBypass()
275
349
 
276
- // Emit event
277
- emitEvent(CallEventType.CALL_ANSWERED, JSONObject().put("callId", callId))
278
- 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
+ })
279
356
 
357
+ notifySpecificCallStateChanged(context, callId, CallState.ACTIVE)
280
358
  Log.d(TAG, "Call $callId successfully answered and UI cleaned up")
281
359
  }
282
360
 
283
- // NEW: Method to update MainActivity lock screen bypass state
284
- private fun updateMainActivityLockScreenBypass() {
285
- try {
286
- // Try to get MainActivity instance through reflection or static reference
287
- val mainActivityClass = Class.forName("com.pingme2022.MainActivity")
288
- val getCurrentInstanceMethod = mainActivityClass.getMethod("getCurrentInstance")
289
- val mainActivityInstance = getCurrentInstanceMethod.invoke(null)
290
-
291
- if (mainActivityInstance != null) {
292
- val updateMethod = mainActivityClass.getMethod("updateLockScreenBypass")
293
- updateMethod.invoke(mainActivityInstance)
294
- Log.d(TAG, "Updated MainActivity lock screen bypass state")
295
- } else {
296
- Log.d(TAG, "MainActivity instance not available for lock screen bypass update")
297
- }
298
- } catch (e: Exception) {
299
- Log.w(TAG, "Could not update MainActivity lock screen bypass: ${e.message}")
300
- }
301
- }
302
-
361
+ // --- Call Control Methods ---
303
362
  fun holdCall(context: Context, callId: String) {
304
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
+
370
+ activeCalls[callId]?.state = CallState.HELD
305
371
  val connection = telecomConnections[callId]
306
372
  connection?.setOnHold()
373
+
374
+ updateForegroundNotification(context)
307
375
  emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
308
- notifyCallStateChanged(context)
376
+ updateLockScreenBypass()
377
+ notifySpecificCallStateChanged(context, callId, CallState.HELD)
309
378
  }
310
379
 
311
380
  fun unholdCall(context: Context, callId: String) {
312
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
+
313
388
  activeCalls[callId]?.state = CallState.ACTIVE
314
389
  val connection = telecomConnections[callId]
315
390
  connection?.setActive()
391
+
392
+ updateForegroundNotification(context)
316
393
  emitEvent(CallEventType.CALL_UNHELD, JSONObject().put("callId", callId))
317
- notifyCallStateChanged(context)
394
+ updateLockScreenBypass()
395
+ notifySpecificCallStateChanged(context, callId, CallState.ACTIVE)
318
396
  }
319
397
 
320
398
  fun muteCall(context: Context, callId: String) {
321
399
  Log.d(TAG, "muteCall: $callId")
322
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
323
404
  audioManager?.isMicrophoneMute = true
324
- 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
+ }
325
410
  }
326
411
 
327
412
  fun unmuteCall(context: Context, callId: String) {
328
413
  Log.d(TAG, "unmuteCall: $callId")
329
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
330
418
  audioManager?.isMicrophoneMute = false
331
- 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
+ }
332
424
  }
333
425
 
334
- // SINGLE SOURCE OF TRUTH: Core function that handles ALL call ending logic
426
+ // --- Call End Management ---
335
427
  fun endCall(context: Context, callId: String) {
336
428
  appContext = context.applicationContext
337
429
  Log.d(TAG, "endCall: $callId")
338
-
339
- // Core cleanup logic
340
430
  coreEndCall(context, callId)
341
431
  }
342
432
 
@@ -346,22 +436,27 @@ object CallEngine {
346
436
  Log.d(TAG, "No active calls, nothing to do.")
347
437
  return
348
438
  }
439
+
349
440
  activeCalls.keys.toList().forEach { callId ->
350
441
  coreEndCall(context, callId)
351
442
  }
443
+
352
444
  activeCalls.clear()
353
445
  telecomConnections.clear()
354
446
  currentCallId = null
355
447
 
356
- // Final cleanup
357
448
  finalCleanup(context)
358
- notifyCallStateChanged(context)
449
+ updateLockScreenBypass()
359
450
  }
360
451
 
361
- // SINGLE SOURCE OF TRUTH: Core function that handles ending a single call
362
452
  private fun coreEndCall(context: Context, callId: String) {
363
453
  Log.d(TAG, "coreEndCall: $callId")
364
454
 
455
+ val callInfo = activeCalls[callId] ?: run {
456
+ Log.w(TAG, "Call $callId not found in active calls")
457
+ return
458
+ }
459
+
365
460
  // Update call state
366
461
  activeCalls[callId]?.state = CallState.ENDED
367
462
  activeCalls.remove(callId)
@@ -390,181 +485,24 @@ object CallEngine {
390
485
  // If no more calls, do final cleanup
391
486
  if (activeCalls.isEmpty()) {
392
487
  finalCleanup(context)
393
- }
394
-
395
- emitEvent(CallEventType.CALL_ENDED, JSONObject().put("callId", callId))
396
- notifyCallStateChanged(context)
397
- }
398
-
399
- // SINGLE SOURCE OF TRUTH: Final cleanup when all calls are ended
400
- private fun finalCleanup(context: Context) {
401
- Log.d(TAG, "Performing final cleanup - no active calls remaining")
402
-
403
- stopForegroundService(context)
404
- keepScreenAwake(context, false)
405
- resetAudioMode(context)
406
-
407
- // ENHANCED: Clear lock screen bypass when calls end
408
- updateMainActivityLockScreenBypass()
409
- }
410
- // NEW: Function to clear lock screen bypass
411
- private fun clearLockScreenBypass(context: Context) {
412
- try {
413
- if (context is Activity) {
414
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
415
- context.setShowWhenLocked(false)
416
- context.setTurnScreenOn(false)
417
- } else {
418
- context.window.clearFlags(
419
- android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
420
- android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
421
- android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
422
- )
423
- }
424
- Log.d(TAG, "Lock screen bypass flags cleared for Activity")
425
- }
426
- } catch (e: Exception) {
427
- Log.w(TAG, "Could not clear lock screen bypass flags: ${e.message}")
428
- }
429
- }
430
-
431
- fun getActiveCalls(): List<CallInfo> = activeCalls.values.toList()
432
- fun getCurrentCallId(): String? = currentCallId
433
- fun isCallActive(): Boolean = activeCalls.any {
434
- it.value.state == CallState.ACTIVE ||
435
- it.value.state == CallState.INCOMING ||
436
- it.value.state == CallState.DIALING
437
- }
438
-
439
- // Enhanced incoming call UI with better cleanup
440
- fun showIncomingCallUI(context: Context, callId: String, callerName: String, callType: String) {
441
- Log.d(TAG, "Showing incoming call UI for $callId, caller: $callerName, callType: $callType")
442
- createNotificationChannel(context)
443
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
444
-
445
- val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
446
- action = "com.qusaieilouti99.callmanager.ANSWER_CALL"
447
- putExtra("callId", callId)
448
- }
449
- val answerPendingIntent = PendingIntent.getBroadcast(context, 0, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
450
-
451
- val declineIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
452
- action = "com.qusaieilouti99.callmanager.DECLINE_CALL"
453
- putExtra("callId", callId)
454
- }
455
- val declinePendingIntent = PendingIntent.getBroadcast(context, 1, declineIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
456
-
457
- val fullScreenIntent = Intent(context, CallActivity::class.java).apply {
458
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
459
- putExtra("callId", callId)
460
- putExtra("callerName", callerName)
461
- putExtra("callType", callType)
462
- }
463
- val fullScreenPendingIntent = PendingIntent.getActivity(context, 2, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
464
-
465
- val notification: Notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
466
- val person = Person.Builder().setName(callerName).setImportant(true).build()
467
- Notification.Builder(context, NOTIF_CHANNEL_ID)
468
- .setSmallIcon(android.R.drawable.sym_call_incoming)
469
- .setStyle(Notification.CallStyle.forIncomingCall(person, declinePendingIntent, answerPendingIntent))
470
- .setFullScreenIntent(fullScreenPendingIntent, true)
471
- .setOngoing(true)
472
- .setAutoCancel(false)
473
- .build()
474
- } else {
475
- Notification.Builder(context, NOTIF_CHANNEL_ID)
476
- .setSmallIcon(android.R.drawable.sym_call_incoming)
477
- .setContentTitle("Incoming Call")
478
- .setContentText(callerName)
479
- .setPriority(Notification.PRIORITY_HIGH)
480
- .setCategory(Notification.CATEGORY_CALL)
481
- .setFullScreenIntent(fullScreenPendingIntent, true)
482
- .addAction(android.R.drawable.sym_action_call, "Answer", answerPendingIntent)
483
- .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Decline", declinePendingIntent)
484
- .setOngoing(true)
485
- .setAutoCancel(false)
486
- .build()
487
- }
488
-
489
- notificationManager.notify(NOTIF_ID, notification)
490
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) playRingtone(context)
491
- if (callType == "Video") {
492
- setAudioRoute(context, "Speaker")
493
- }
494
- }
495
-
496
- fun cancelIncomingCallUI(context: Context) {
497
- Log.d(TAG, "Cancelling incoming call UI.")
498
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
499
- notificationManager.cancel(NOTIF_ID)
500
- stopRingtone()
501
- }
502
-
503
- fun startForegroundService(context: Context) {
504
- Log.d(TAG, "Starting CallForegroundService.")
505
- val intent = Intent(context, CallForegroundService::class.java)
506
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent)
507
- else context.startService(intent)
508
- }
509
-
510
- fun stopForegroundService(context: Context) {
511
- Log.d(TAG, "Stopping CallForegroundService.")
512
- val intent = Intent(context, CallForegroundService::class.java)
513
- context.stopService(intent)
514
- }
515
-
516
- // Enhanced bringAppToForeground with better lock screen handling
517
- fun bringAppToForeground(context: Context) {
518
- val packageName = context.packageName
519
- val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
520
- launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
521
-
522
- // Handle lock screen bypass for active calls
523
- if (isCallActive()) {
524
- // ENHANCED: Add multiple flags for better lock screen bypass
525
- launchIntent?.putExtra("BYPASS_LOCK_SCREEN", true)
526
-
527
- // Add lock screen bypass flags directly to the intent
528
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
529
- launchIntent?.addFlags(Intent.FLAG_ACTIVITY_SHOW_WHEN_LOCKED or Intent.FLAG_ACTIVITY_TURN_SCREEN_ON)
530
- } else {
531
- launchIntent?.addFlags(
532
- Intent.FLAG_ACTIVITY_SHOW_WHEN_LOCKED or
533
- Intent.FLAG_ACTIVITY_TURN_SCREEN_ON or
534
- Intent.FLAG_ACTIVITY_CLEAR_TASK
535
- )
536
- }
537
-
538
- Log.d(TAG, "App brought to foreground with lock screen bypass for active call")
539
488
  } else {
540
- launchIntent?.removeExtra("BYPASS_LOCK_SCREEN")
541
- Log.d(TAG, "App brought to foreground without lock screen bypass")
489
+ updateForegroundNotification(context)
542
490
  }
543
491
 
544
- try {
545
- context.startActivity(launchIntent)
546
-
547
- // ADDED: Small delay to ensure activity is created before updating bypass
548
- android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
549
- updateMainActivityLockScreenBypass()
550
- }, 100)
551
-
552
- } catch (e: Exception) {
553
- Log.e(TAG, "Failed to bring app to foreground: ${e.message}")
554
- }
492
+ updateLockScreenBypass()
493
+ emitEvent(CallEventType.CALL_ENDED, JSONObject().put("callId", callId))
494
+ notifySpecificCallStateChanged(context, callId, CallState.ENDED)
555
495
  }
556
496
 
557
- // Rest of the methods remain the same...
558
- // (getAudioDevices, setAudioRoute, keepScreenAwake, etc. - keeping them unchanged for brevity)
559
-
560
- // --- Audio Device Management ---
497
+ // --- Audio Management ---
561
498
  fun getAudioDevices(): AudioRoutesInfo {
562
499
  audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
563
500
  Log.e(TAG, "getAudioDevices: AudioManager is null or appContext is not set. Returning default.")
564
501
  return AudioRoutesInfo(emptyArray(), "Unknown")
565
502
  }
566
- val devices = mutableListOf<String>()
567
- var currentRoute: String = "Unknown"
503
+
504
+ val devices = mutableSetOf<String>()
505
+ var currentRoute = "Earpiece" // Default
568
506
 
569
507
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
570
508
  val audioDeviceInfoList = audioManager?.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
@@ -572,47 +510,31 @@ object CallEngine {
572
510
  when (device.type) {
573
511
  AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> {
574
512
  devices.add("Bluetooth")
575
- if (audioManager?.isBluetoothScoOn == true && !device.isSource) currentRoute = "Bluetooth"
576
513
  }
577
514
  AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET -> {
578
515
  devices.add("Headset")
579
- if (audioManager?.isWiredHeadsetOn == true && !device.isSource) currentRoute = "Headset"
580
516
  }
581
517
  AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> {
582
518
  devices.add("Speaker")
583
- if (audioManager?.isSpeakerphoneOn == true && !device.isSource) currentRoute = "Speaker"
584
519
  }
585
520
  AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> {
586
521
  devices.add("Earpiece")
587
- if (audioManager?.isSpeakerphoneOn == false && audioManager?.isWiredHeadsetOn == false && audioManager?.isBluetoothScoOn == false && !device.isSource) {
588
- currentRoute = "Earpiece"
589
- }
590
522
  }
591
- else -> Log.d(TAG, "Unknown audio device type: ${device.type}")
592
523
  }
593
524
  }
594
525
  } else {
595
- devices.add("Speaker")
596
- devices.add("Earpiece")
597
- if (audioManager?.isSpeakerphoneOn == true) currentRoute = "Speaker"
598
- else currentRoute = "Earpiece"
526
+ devices.addAll(listOf("Speaker", "Earpiece"))
599
527
  }
600
528
 
601
- val distinctDevices = devices.distinct().toTypedArray()
602
-
603
- if (currentRoute == "Unknown" || currentRoute == "Earpiece") {
604
- if (audioManager?.isBluetoothScoOn == true) {
605
- currentRoute = "Bluetooth"
606
- } else if (audioManager?.isSpeakerphoneOn == true) {
607
- currentRoute = "Speaker"
608
- } else if (audioManager?.isWiredHeadsetOn == true) {
609
- currentRoute = "Headset"
610
- } else {
611
- currentRoute = "Earpiece"
612
- }
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"
613
535
  }
614
536
 
615
- val result = AudioRoutesInfo(distinctDevices, currentRoute)
537
+ val result = AudioRoutesInfo(devices.toTypedArray(), currentRoute)
616
538
  Log.d(TAG, "Audio devices info: $result")
617
539
  return result
618
540
  }
@@ -621,6 +543,9 @@ object CallEngine {
621
543
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
622
544
  Log.d(TAG, "Attempting to set audio route to: $route. Current mode: ${audioManager?.mode}")
623
545
 
546
+ val previousRoute = getCurrentAudioRoute()
547
+
548
+ // Reset all routes first
624
549
  audioManager?.isSpeakerphoneOn = false
625
550
  audioManager?.stopBluetoothSco()
626
551
  audioManager?.isBluetoothScoOn = false
@@ -633,7 +558,6 @@ object CallEngine {
633
558
  }
634
559
  "Earpiece" -> {
635
560
  Log.d(TAG, "Setting audio route to Earpiece.")
636
- audioManager?.isSpeakerphoneOn = false
637
561
  audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
638
562
  }
639
563
  "Bluetooth" -> {
@@ -648,9 +572,43 @@ object CallEngine {
648
572
  }
649
573
  else -> {
650
574
  Log.w(TAG, "Unknown audio route: $route. No action taken.")
575
+ return
651
576
  }
652
577
  }
653
- 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)
654
612
  }
655
613
 
656
614
  fun resetAudioMode(context: Context) {
@@ -666,7 +624,51 @@ object CallEngine {
666
624
  }
667
625
  }
668
626
 
669
- // --- 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 ---
670
672
  fun keepScreenAwake(context: Context, keepAwake: Boolean) {
671
673
  val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
672
674
  if (keepAwake) {
@@ -677,8 +679,6 @@ object CallEngine {
677
679
  )
678
680
  wakeLock?.acquire(10 * 60 * 1000L /* 10 minutes */)
679
681
  Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK.")
680
- } else {
681
- Log.d(TAG, "Wake lock already held.")
682
682
  }
683
683
  } else {
684
684
  wakeLock?.let {
@@ -691,61 +691,58 @@ object CallEngine {
691
691
  }
692
692
  }
693
693
 
694
- // --- Audio Device Change Listener ---
695
- private val audioDeviceCallback = object : AudioDeviceCallback() {
696
- override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
697
- Log.d(TAG, "Audio devices added. Emitting AUDIO_DEVICES_CHANGED.")
698
- emitAudioDevicesChanged()
699
- }
700
- override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
701
- Log.d(TAG, "Audio devices removed. Emitting AUDIO_DEVICES_CHANGED.")
702
- emitAudioDevicesChanged()
703
- }
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
704
701
  }
705
702
 
706
- fun registerAudioDeviceCallback(context: Context) {
707
- appContext = context.applicationContext
708
- audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
709
- audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
710
- 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
+ }
711
707
  }
712
708
 
713
- fun unregisterAudioDeviceCallback(context: Context) {
714
- audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
715
- audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
716
- 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
+ }
717
715
  }
718
716
 
719
- fun emitAudioDevicesChanged() {
720
- val context = appContext ?: run {
721
- Log.w(TAG, "Cannot emit AudioDevicesChanged: appContext is null.")
722
- return
723
- }
724
- val audioInfo = getAudioDevices()
725
- val jsonPayload = JSONObject().apply {
726
- put("devices", JSONArray(audioInfo.devices.toList()))
727
- 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"
728
722
  }
729
- emitEvent(CallEventType.AUDIO_DEVICES_CHANGED, jsonPayload)
730
723
  }
731
724
 
732
- // --- Call State Change Notification ---
733
- private fun notifyCallStateChanged(context: Context) {
734
- val calls = getActiveCalls()
735
- val jsonArray = JSONArray()
736
- calls.forEach {
737
- val obj = JSONObject()
738
- obj.put("callId", it.callId)
739
- obj.put("callData", it.callData)
740
- obj.put("state", it.state.name)
741
- obj.put("callType", it.callType)
742
- 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
+ }
743
736
  }
744
- val jsonPayload = JSONObject().put("calls", jsonArray)
745
- Log.d(TAG, "Call state changed. Emitting CALL_STATE_CHANGED: $jsonPayload")
746
- 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
+ })
747
743
  }
748
744
 
745
+ // --- Notification Management ---
749
746
  private fun createNotificationChannel(context: Context) {
750
747
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
751
748
  val channel = NotificationChannel(
@@ -757,6 +754,7 @@ object CallEngine {
757
754
  channel.enableLights(true)
758
755
  channel.lightColor = Color.GREEN
759
756
  channel.enableVibration(true)
757
+
760
758
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
761
759
  channel.setSound(
762
760
  RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE),
@@ -769,20 +767,138 @@ object CallEngine {
769
767
  channel.setSound(null, null)
770
768
  channel.importance = NotificationManager.IMPORTANCE_HIGH
771
769
  }
770
+
772
771
  val manager = context.getSystemService(NotificationManager::class.java)
773
772
  manager.createNotificationChannel(channel)
774
773
  Log.d(TAG, "Notification channel '$NOTIF_CHANNEL_ID' created/updated.")
775
774
  }
776
775
  }
777
776
 
778
- // 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 ---
779
893
  private fun registerPhoneAccount(context: Context) {
780
894
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
781
895
  val phoneAccountHandle = getPhoneAccountHandle(context)
896
+
782
897
  if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
783
898
  val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
784
899
  .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
785
900
  .build()
901
+
786
902
  try {
787
903
  telecomManager.registerPhoneAccount(phoneAccount)
788
904
  Log.d(TAG, "PhoneAccount registered successfully.")
@@ -803,12 +919,13 @@ object CallEngine {
803
919
  )
804
920
  }
805
921
 
806
- // --- Ringtone Management (for incoming calls, pre-Android S) ---
922
+ // --- Media Management ---
807
923
  fun playRingtone(context: Context) {
808
924
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
809
925
  Log.d(TAG, "playRingtone: Android S+ detected, system will handle ringtone via Telecom.")
810
926
  return
811
927
  }
928
+
812
929
  try {
813
930
  Log.d(TAG, "Playing ringtone (for Android < S).")
814
931
  val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
@@ -825,7 +942,7 @@ object CallEngine {
825
942
 
826
943
  fun stopRingtone() {
827
944
  try {
828
- if (ringtone != null && ringtone!!.isPlaying) {
945
+ if (ringtone?.isPlaying == true) {
829
946
  ringtone?.stop()
830
947
  Log.d(TAG, "Ringtone stopped.")
831
948
  }
@@ -835,12 +952,12 @@ object CallEngine {
835
952
  ringtone = null
836
953
  }
837
954
 
838
- // --- Ringback Tone Management (for outgoing calls) ---
839
955
  private fun startRingback() {
840
- if (ringbackPlayer != null && ringbackPlayer!!.isPlaying) {
956
+ if (ringbackPlayer?.isPlaying == true) {
841
957
  Log.d(TAG, "Ringback tone already playing.")
842
958
  return
843
959
  }
960
+
844
961
  try {
845
962
  val ringbackUri = Uri.parse("android.resource://${appContext?.packageName}/raw/ringback_tone")
846
963
  ringbackPlayer = MediaPlayer.create(appContext, ringbackUri)
@@ -848,6 +965,7 @@ object CallEngine {
848
965
  Log.e(TAG, "Failed to create MediaPlayer for ringback. Check raw/ringback_tone.mp3 exists.")
849
966
  return
850
967
  }
968
+
851
969
  ringbackPlayer?.apply {
852
970
  isLooping = true
853
971
  setAudioAttributes(
@@ -866,7 +984,7 @@ object CallEngine {
866
984
 
867
985
  private fun stopRingback() {
868
986
  try {
869
- if (ringbackPlayer != null && ringbackPlayer!!.isPlaying) {
987
+ if (ringbackPlayer?.isPlaying == true) {
870
988
  ringbackPlayer?.stop()
871
989
  ringbackPlayer?.release()
872
990
  Log.d(TAG, "Ringback tone stopped and released.")
@@ -877,4 +995,46 @@ object CallEngine {
877
995
  ringbackPlayer = null
878
996
  }
879
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
+ }
880
1040
  }