@qusaieilouti99/call-manager 0.1.147 → 0.1.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,6 @@
1
1
  package com.margelo.nitro.qusaieilouti99.callmanager
2
2
 
3
3
  import android.app.ActivityManager
4
- import android.app.KeyguardManager
5
4
  import android.app.Notification
6
5
  import android.app.NotificationChannel
7
6
  import android.app.NotificationManager
@@ -9,6 +8,8 @@ import android.app.PendingIntent
9
8
  import android.content.ComponentName
10
9
  import android.content.Context
11
10
  import android.content.Intent
11
+ import android.graphics.Color
12
+ import android.media.AudioAttributes
12
13
  import android.media.AudioDeviceCallback
13
14
  import android.media.AudioDeviceInfo
14
15
  import android.media.AudioManager
@@ -20,8 +21,6 @@ import android.os.Bundle
20
21
  import android.os.Handler
21
22
  import android.os.Looper
22
23
  import android.os.PowerManager
23
- import android.os.VibrationEffect
24
- import android.os.Vibrator
25
24
  import android.telecom.Connection
26
25
  import android.telecom.DisconnectCause
27
26
  import android.telecom.PhoneAccount
@@ -33,857 +32,1357 @@ import org.json.JSONObject
33
32
  import java.util.concurrent.ConcurrentHashMap
34
33
  import java.util.concurrent.CopyOnWriteArrayList
35
34
  import java.util.concurrent.atomic.AtomicBoolean
35
+ import android.app.KeyguardManager
36
+ import android.os.Vibrator
37
+ import android.os.VibrationEffect
36
38
 
39
+ /**
40
+ * Core call‐management engine. Manages self-managed telecom calls,
41
+ * audio routing, UI notifications, etc.
42
+ *
43
+ * NOTE: Volume key silencing is now handled by the system via `Connection.onSilence()`,
44
+ * which calls `silenceIncomingCall()` on this object.
45
+ */
37
46
  object CallEngine {
38
- private const val TAG = "CallEngine"
39
- private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
40
- private const val NOTIF_CHANNEL_ID = "incoming_call_channel"
41
- private const val NOTIF_ID = 2001
42
-
43
- interface CallEndListener {
44
- fun onCallEnded(callId: String)
47
+ private const val TAG = "CallEngine"
48
+ private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
49
+ private const val NOTIF_CHANNEL_ID = "incoming_call_channel"
50
+ private const val NOTIF_ID = 2001
51
+
52
+ interface CallEndListener {
53
+ fun onCallEnded(callId: String)
54
+ }
55
+
56
+ private val callEndListeners = CopyOnWriteArrayList<CallEndListener>()
57
+ private val mainHandler = Handler(Looper.getMainLooper())
58
+
59
+ fun registerCallEndListener(l: CallEndListener) {
60
+ callEndListeners.add(l)
61
+ }
62
+
63
+ fun unregisterCallEndListener(l: CallEndListener) {
64
+ callEndListeners.remove(l)
65
+ }
66
+
67
+ @Volatile private var appContext: Context? = null
68
+ private val isInitialized = AtomicBoolean(false)
69
+ private val initializationLock = Any()
70
+
71
+ private var ringtone: android.media.Ringtone? = null
72
+ private var ringbackPlayer: MediaPlayer? = null
73
+ private var vibrator: Vibrator? = null
74
+ private var audioManager: AudioManager? = null
75
+ private var wakeLock: PowerManager.WakeLock? = null
76
+
77
+ private val activeCalls = ConcurrentHashMap<String, CallInfo>()
78
+ private val telecomConnections = ConcurrentHashMap<String, Connection>()
79
+ private val callMetadata = ConcurrentHashMap<String, String>()
80
+
81
+ // NEW: Track incoming calls to prevent duplicates
82
+ private val incomingCallIds = ConcurrentHashMap<String, Long>()
83
+
84
+ private var currentCallId: String? = null
85
+ private var canMakeMultipleCalls: Boolean = false
86
+ private var lastAudioRoutesInfo: AudioRoutesInfo? = null
87
+ private var lockScreenBypassActive = false
88
+ private val lockScreenBypassCallbacks = mutableSetOf<LockScreenBypassCallback>()
89
+ private var eventHandler: ((CallEventType, String) -> Unit)? = null
90
+ private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
91
+
92
+ // NEW: Track ringtone state to prevent double ringing
93
+ private var isCustomRingtoneActive = false
94
+ private val ringtoneStateLock = Any()
95
+
96
+ interface LockScreenBypassCallback {
97
+ fun onLockScreenBypassChanged(shouldBypass: Boolean)
98
+ }
99
+
100
+ fun initialize(context: Context) {
101
+ synchronized(initializationLock) {
102
+ if (isInitialized.compareAndSet(false, true)) {
103
+ appContext = context.applicationContext
104
+ audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
105
+ Log.d(TAG, "CallEngine initialized successfully")
106
+ if (isCallActive()) {
107
+ startForegroundService()
108
+ }
109
+ }
45
110
  }
46
-
47
- interface LockScreenBypassCallback {
48
- fun onLockScreenBypassChanged(shouldBypass: Boolean)
111
+ }
112
+
113
+ fun isInitialized(): Boolean = isInitialized.get()
114
+
115
+ private fun requireContext(): Context {
116
+ return appContext ?: throw IllegalStateException(
117
+ "CallEngine not initialized. Call initialize() in Application.onCreate()"
118
+ )
119
+ }
120
+
121
+ /**
122
+ * Get the application context. Returns null if not initialized.
123
+ */
124
+ fun getContext(): Context? = appContext
125
+
126
+ fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
127
+ Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
128
+ eventHandler = handler
129
+ handler?.let { h ->
130
+ if (cachedEvents.isNotEmpty()) {
131
+ Log.d(TAG, "Emitting ${cachedEvents.size} cached events.")
132
+ cachedEvents.forEach { (type, data) -> h.invoke(type, data) }
133
+ cachedEvents.clear()
134
+ }
49
135
  }
50
-
51
- private val callEndListeners = CopyOnWriteArrayList<CallEndListener>()
52
- private val mainHandler = Handler(Looper.getMainLooper())
53
-
54
- fun registerCallEndListener(l: CallEndListener) {
55
- callEndListeners.add(l)
136
+ }
137
+
138
+ fun emitEvent(type: CallEventType, data: JSONObject) {
139
+ Log.d(TAG, "Emitting event: $type")
140
+ val dataString = data.toString()
141
+ if (eventHandler != null) {
142
+ eventHandler?.invoke(type, dataString)
143
+ } else {
144
+ Log.d(TAG, "No event handler, caching event: $type")
145
+ cachedEvents.add(Pair(type, dataString))
56
146
  }
57
-
58
- fun unregisterCallEndListener(l: CallEndListener) {
59
- callEndListeners.remove(l)
147
+ }
148
+
149
+ /**
150
+ * NEW: Check if device supports CallStyle notifications
151
+ */
152
+ private fun supportsCallStyleNotifications(): Boolean {
153
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
154
+
155
+ val manufacturer = Build.MANUFACTURER.lowercase()
156
+ val brand = Build.BRAND.lowercase()
157
+
158
+ // Known good manufacturers that support CallStyle properly
159
+ val supportedManufacturers = setOf(
160
+ "google", "samsung", "oneplus", "motorola", "sony", "lg", "htc"
161
+ )
162
+
163
+ val supportedBrands = setOf(
164
+ "google", "samsung", "oneplus", "motorola", "sony", "lg", "htc", "pixel"
165
+ )
166
+
167
+ val isSupported = supportedManufacturers.contains(manufacturer) ||
168
+ supportedBrands.contains(brand) ||
169
+ manufacturer.contains("google") ||
170
+ brand.contains("pixel")
171
+
172
+ Log.d(TAG, "CallStyle support check - Manufacturer: $manufacturer, Brand: $brand, Supported: $isSupported")
173
+ return isSupported
174
+ }
175
+
176
+ /**
177
+ * Silences the incoming call ringtone. This is called by `Connection.onSilence()`
178
+ * when the user presses a volume key during ringing.
179
+ */
180
+ fun silenceIncomingCall() {
181
+ Log.d(TAG, "Silencing incoming call ringtone via Connection.onSilence()")
182
+ stopRingtone()
183
+ }
184
+
185
+ fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
186
+ lockScreenBypassCallbacks.add(callback)
187
+ }
188
+
189
+ fun unregisterLockScreenBypassCallback(callback: LockScreenBypassCallback) {
190
+ lockScreenBypassCallbacks.remove(callback)
191
+ }
192
+
193
+ private fun updateLockScreenBypass() {
194
+ val shouldBypass = isCallActive()
195
+ if (lockScreenBypassActive != shouldBypass) {
196
+ lockScreenBypassActive = shouldBypass
197
+ Log.d(TAG, "Lock screen bypass state changed: $lockScreenBypassActive")
198
+ lockScreenBypassCallbacks.forEach { callback ->
199
+ try {
200
+ callback.onLockScreenBypassChanged(shouldBypass)
201
+ } catch (e: Exception) {
202
+ Log.w(TAG, "Error notifying lock screen bypass callback", e)
203
+ }
204
+ }
60
205
  }
206
+ }
61
207
 
62
- @Volatile private var appContext: Context? = null
63
- private val isInitialized = AtomicBoolean(false)
64
- private val initializationLock = Any()
65
-
66
- private var ringtone: android.media.Ringtone? = null
67
- private var ringbackPlayer: MediaPlayer? = null
68
- private var vibrator: Vibrator? = null
69
- private var audioManager: AudioManager? = null
70
- private var wakeLock: PowerManager.WakeLock? = null
208
+ fun isLockScreenBypassActive(): Boolean = lockScreenBypassActive
71
209
 
72
- private val activeCalls = ConcurrentHashMap<String, CallInfo>()
73
- private val telecomConnections = ConcurrentHashMap<String, Connection>()
74
- private val callMetadata = ConcurrentHashMap<String, String>()
75
- private val incomingCallIds = ConcurrentHashMap<String, Long>()
210
+ fun addTelecomConnection(callId: String, connection: Connection) {
211
+ telecomConnections[callId] = connection
212
+ Log.d(TAG, "Added Telecom Connection for callId: $callId")
213
+ }
76
214
 
77
- private var currentCallId: String? = null
78
- private var canMakeMultipleCalls: Boolean = false
79
- private var eventHandler: ((CallEventType, String) -> Unit)? = null
80
- private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
215
+ fun removeTelecomConnection(callId: String) {
216
+ telecomConnections.remove(callId)
217
+ Log.d(TAG, "Removed Telecom Connection for callId: $callId")
218
+ }
81
219
 
82
- private var userSelectedAudioRoute: String? = null
83
- private val audioStateLock = Any()
220
+ fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
84
221
 
85
- private var lockScreenBypassActive = false
86
- private val lockScreenBypassCallbacks = mutableSetOf<LockScreenBypassCallback>()
222
+ fun setCanMakeMultipleCalls(allow: Boolean) {
223
+ canMakeMultipleCalls = allow
224
+ Log.d(TAG, "canMakeMultipleCalls set to: $allow")
225
+ }
87
226
 
88
- fun initialize(context: Context) {
89
- synchronized(initializationLock) {
90
- if (isInitialized.compareAndSet(false, true)) {
91
- appContext = context.applicationContext
92
- audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
93
- registerAudioDeviceCallback()
94
- Log.d(TAG, "CallEngine initialized successfully")
95
- }
96
- }
227
+ fun getCurrentCallState(): String {
228
+ val calls = getActiveCalls()
229
+ val jsonArray = JSONArray()
230
+ calls.forEach {
231
+ jsonArray.put(it.toJsonObject())
232
+ }
233
+ return jsonArray.toString()
234
+ }
235
+
236
+ fun reportIncomingCall(
237
+ context: Context,
238
+ callId: String,
239
+ callType: String,
240
+ displayName: String,
241
+ pictureUrl: String? = null,
242
+ metadata: String? = null
243
+ ) {
244
+ if (!isInitialized.get()) {
245
+ initialize(context)
97
246
  }
98
247
 
99
- fun isInitialized(): Boolean = isInitialized.get()
100
-
101
- private fun requireContext(): Context {
102
- return appContext ?: throw IllegalStateException(
103
- "CallEngine not initialized. Call initialize() in Application.onCreate()"
104
- )
248
+ // NEW: Guard against duplicate calls
249
+ val currentTime = System.currentTimeMillis()
250
+ val lastCallTime = incomingCallIds[callId]
251
+ if (lastCallTime != null && (currentTime - lastCallTime) < 5000) {
252
+ Log.w(TAG, "Ignoring duplicate incoming call for callId: $callId (last call ${currentTime - lastCallTime}ms ago)")
253
+ return
105
254
  }
255
+ incomingCallIds[callId] = currentTime
106
256
 
107
- fun getContext(): Context? = appContext
257
+ // Clean up old entries (older than 30 seconds)
258
+ val cutoffTime = currentTime - 30000
259
+ incomingCallIds.entries.removeAll { it.value < cutoffTime }
108
260
 
109
- fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
110
- Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
111
- eventHandler = handler
112
- handler?.let { h ->
113
- if (cachedEvents.isNotEmpty()) {
114
- Log.d(TAG, "Emitting ${cachedEvents.size} cached events.")
115
- cachedEvents.forEach { (type, data) -> h.invoke(type, data) }
116
- cachedEvents.clear()
117
- }
118
- }
261
+ Log.d(TAG, "reportIncomingCall: callId=$callId, type=$callType, name=$displayName")
262
+ metadata?.let { callMetadata[callId] = it }
263
+
264
+ val incomingCall = activeCalls.values.find { it.state == CallState.INCOMING }
265
+ if (incomingCall != null && incomingCall.callId != callId) {
266
+ Log.d(TAG, "Incoming call collision detected. Auto-rejecting new call: $callId")
267
+ rejectIncomingCallCollision(callId, "Another call is already incoming")
268
+ return
119
269
  }
120
270
 
121
- fun emitEvent(type: CallEventType, data: JSONObject) {
122
- Log.d(TAG, "Emitting event: $type")
123
- val dataString = data.toString()
124
- if (eventHandler != null) {
125
- eventHandler?.invoke(type, dataString)
126
- } else {
127
- Log.d(TAG, "No event handler, caching event: $type")
128
- cachedEvents.add(Pair(type, dataString))
129
- }
271
+ val activeCall = activeCalls.values.find {
272
+ it.state == CallState.ACTIVE || it.state == CallState.HELD
273
+ }
274
+ if (activeCall != null && !canMakeMultipleCalls) {
275
+ Log.d(TAG, "Active call exists when receiving incoming call. Auto-rejecting: $callId")
276
+ rejectIncomingCallCollision(callId, "Another call is already active")
277
+ return
130
278
  }
131
279
 
132
- private fun supportsCallStyleNotifications(): Boolean {
133
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
134
- val manufacturer = Build.MANUFACTURER.lowercase()
135
- val supportedManufacturers = setOf("google", "samsung")
136
- return supportedManufacturers.contains(manufacturer)
280
+ val isVideoCall = callType == "Video"
281
+ if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
282
+ activeCalls.values.forEach {
283
+ if (it.state == CallState.ACTIVE) {
284
+ holdCallInternal(it.callId, heldBySystem = false)
285
+ }
286
+ }
137
287
  }
138
288
 
139
- fun silenceIncomingCall() {
140
- Log.d(TAG, "Silencing incoming call ringtone via Connection.onSilence()")
141
- stopRingtone()
289
+ activeCalls[callId] =
290
+ CallInfo(callId, callType, displayName, pictureUrl, CallState.INCOMING)
291
+ currentCallId = callId
292
+ Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING")
293
+
294
+ showIncomingCallUI(callId, displayName, callType, pictureUrl)
295
+ registerPhoneAccount()
296
+
297
+ val telecomManager =
298
+ requireContext().getSystemService(Context.TELECOM_SERVICE) as TelecomManager
299
+ val phoneAccountHandle = getPhoneAccountHandle()
300
+ val extras = Bundle().apply {
301
+ putString(MyConnectionService.EXTRA_CALL_ID, callId)
302
+ putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
303
+ putString(MyConnectionService.EXTRA_DISPLAY_NAME, displayName)
304
+ putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, isVideoCall)
305
+ pictureUrl?.let { putString(MyConnectionService.EXTRA_PICTURE_URL, it) }
142
306
  }
143
307
 
144
- fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
145
- lockScreenBypassCallbacks.add(callback)
308
+ try {
309
+ telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
310
+ startForegroundService()
311
+ Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
312
+ } catch (e: Exception) {
313
+ Log.e(TAG, "Failed to report incoming call: ${e.message}", e)
314
+ endCallInternal(callId)
146
315
  }
147
316
 
148
- fun unregisterLockScreenBypassCallback(callback: LockScreenBypassCallback) {
149
- lockScreenBypassCallbacks.remove(callback)
317
+ updateLockScreenBypass()
318
+ }
319
+
320
+ fun startOutgoingCall(
321
+ callId: String,
322
+ callType: String,
323
+ targetName: String,
324
+ metadata: String? = null
325
+ ) {
326
+ val context = requireContext()
327
+ Log.d(TAG, "startOutgoingCall: callId=$callId, type=$callType, target=$targetName")
328
+ metadata?.let { callMetadata[callId] = it }
329
+
330
+ if (!validateOutgoingCallRequest()) {
331
+ Log.w(TAG, "Rejecting outgoing call - incoming/active call exists")
332
+ emitEvent(CallEventType.CALL_REJECTED, JSONObject().apply {
333
+ put("callId", callId)
334
+ put("reason", "Cannot start outgoing call while incoming or active call exists")
335
+ })
336
+ return
150
337
  }
151
338
 
152
- private fun updateLockScreenBypass() {
153
- val shouldBypass = isCallActive()
154
- if (lockScreenBypassActive != shouldBypass) {
155
- lockScreenBypassActive = shouldBypass
156
- Log.d(TAG, "Lock screen bypass state changed: $lockScreenBypassActive")
157
- lockScreenBypassCallbacks.forEach { callback ->
158
- try {
159
- callback.onLockScreenBypassChanged(shouldBypass)
160
- } catch (e: Exception) {
161
- Log.w(TAG, "Error notifying lock screen bypass callback", e)
162
- }
163
- }
339
+ val isVideoCall = callType == "Video"
340
+ if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
341
+ activeCalls.values.forEach {
342
+ if (it.state == CallState.ACTIVE) {
343
+ holdCallInternal(it.callId, heldBySystem = false)
164
344
  }
345
+ }
165
346
  }
166
347
 
167
- fun isLockScreenBypassActive(): Boolean = lockScreenBypassActive
348
+ activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.DIALING)
349
+ currentCallId = callId
350
+ Log.d(TAG, "Call $callId added to activeCalls. State: DIALING")
351
+
352
+ setAudioMode()
353
+ registerPhoneAccount()
354
+
355
+ val telecomManager =
356
+ context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
357
+ val phoneAccountHandle = getPhoneAccountHandle()
358
+ val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
168
359
 
169
- fun addTelecomConnection(callId: String, connection: Connection) {
170
- telecomConnections[callId] = connection
360
+ val outgoingExtras = Bundle().apply {
361
+ putString(MyConnectionService.EXTRA_CALL_ID, callId)
362
+ putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
363
+ putString(MyConnectionService.EXTRA_DISPLAY_NAME, targetName)
364
+ putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, isVideoCall)
365
+ metadata?.let { putString("metadata", it) }
171
366
  }
172
367
 
173
- fun removeTelecomConnection(callId: String) {
174
- telecomConnections.remove(callId)
368
+ val extras = Bundle().apply {
369
+ putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
370
+ putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
371
+ putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, isVideoCall)
175
372
  }
176
373
 
177
- fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
374
+ try {
375
+ telecomManager.placeCall(addressUri, extras)
376
+ startForegroundService()
377
+ startRingback()
378
+ bringAppToForeground()
379
+ keepScreenAwake(true)
380
+ Log.d(TAG, "Successfully reported outgoing call to TelecomManager")
381
+ } catch (e: Exception) {
382
+ Log.e(TAG, "Failed to start outgoing call: ${e.message}", e)
383
+ endCallInternal(callId)
384
+ }
178
385
 
179
- fun reportIncomingCall(
180
- context: Context,
181
- callId: String,
182
- callType: String,
183
- displayName: String,
184
- pictureUrl: String? = null,
185
- metadata: String? = null
186
- ) {
187
- if (!isInitialized.get()) {
188
- initialize(context)
189
- }
386
+ updateLockScreenBypass()
387
+ }
388
+
389
+ fun startCall(
390
+ callId: String,
391
+ callType: String,
392
+ targetName: String,
393
+ metadata: String? = null
394
+ ) {
395
+ Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
396
+ metadata?.let { callMetadata[callId] = it }
397
+
398
+ if (activeCalls.containsKey(callId)) {
399
+ Log.w(TAG, "Call $callId already exists, cannot start again")
400
+ return
401
+ }
190
402
 
191
- val currentTime = System.currentTimeMillis()
192
- if (incomingCallIds.containsKey(callId) || activeCalls.containsKey(callId)) {
193
- Log.w(TAG, "Ignoring duplicate incoming call report for callId: $callId")
194
- return
195
- }
196
- incomingCallIds[callId] = currentTime
197
-
198
- Log.d(TAG, "reportIncomingCall: callId=$callId, type=$callType, name=$displayName")
199
- metadata?.let { callMetadata[callId] = it }
200
-
201
- activeCalls[callId] =
202
- CallInfo(callId, callType, displayName, pictureUrl, CallState.INCOMING)
203
- currentCallId = callId
204
-
205
- showIncomingCallUI(callId, displayName, callType, pictureUrl)
206
- registerPhoneAccount()
207
-
208
- val telecomManager =
209
- requireContext().getSystemService(Context.TELECOM_SERVICE) as TelecomManager
210
- val phoneAccountHandle = getPhoneAccountHandle()
211
- val extras = Bundle().apply {
212
- putString(MyConnectionService.EXTRA_CALL_ID, callId)
213
- putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
214
- putString(MyConnectionService.EXTRA_DISPLAY_NAME, displayName)
215
- putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, callType == "Video")
216
- pictureUrl?.let { putString(MyConnectionService.EXTRA_PICTURE_URL, it) }
403
+ if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
404
+ activeCalls.values.forEach {
405
+ if (it.state == CallState.ACTIVE) {
406
+ holdCallInternal(it.callId, heldBySystem = false)
217
407
  }
408
+ }
409
+ }
218
410
 
219
- try {
220
- telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
221
- startForegroundService()
222
- } catch (e: Exception) {
223
- Log.e(TAG, "Failed to report incoming call: ${e.message}", e)
224
- endCallInternal(callId)
225
- }
226
- updateLockScreenBypass()
411
+ activeCalls[callId] =
412
+ CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
413
+ currentCallId = callId
414
+ Log.d(TAG, "Call $callId started as ACTIVE")
415
+
416
+ registerPhoneAccount()
417
+ setAudioMode()
418
+ bringAppToForeground()
419
+ startForegroundService()
420
+ keepScreenAwake(true)
421
+
422
+ // NEW: Improved initial audio route setting with better timing
423
+ mainHandler.postDelayed({
424
+ setInitialAudioRoute(callType, isCallStart = true)
425
+ }, 500L)
426
+
427
+ updateLockScreenBypass()
428
+ emitOutgoingCallAnsweredWithMetadata(callId)
429
+ }
430
+
431
+ fun callAnsweredFromJS(callId: String) {
432
+ Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
433
+ coreCallAnswered(callId, isLocalAnswer = false)
434
+ }
435
+
436
+ fun answerCall(callId: String) {
437
+ Log.d(TAG, "answerCall: $callId - local party answering")
438
+ coreCallAnswered(callId, isLocalAnswer = true)
439
+ }
440
+
441
+ private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
442
+ Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
443
+ val callInfo = activeCalls[callId]
444
+ if (callInfo == null) {
445
+ Log.w(TAG, "Cannot answer call $callId - not found in active calls")
446
+ return
227
447
  }
228
448
 
229
- fun startOutgoingCall(
230
- callId: String,
231
- callType: String,
232
- targetName: String,
233
- metadata: String? = null
234
- ) {
235
- val context = requireContext()
236
- Log.d(TAG, "startOutgoingCall: callId=$callId, type=$callType, target=$targetName")
237
- metadata?.let { callMetadata[callId] = it }
238
-
239
- activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.DIALING)
240
- currentCallId = callId
241
- Log.d(TAG, "Call $callId added to activeCalls. State: DIALING")
242
-
243
- registerPhoneAccount()
244
-
245
- val telecomManager =
246
- context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
247
- val phoneAccountHandle = getPhoneAccountHandle()
248
- val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
249
-
250
- val outgoingExtras = Bundle().apply {
251
- putString(MyConnectionService.EXTRA_CALL_ID, callId)
252
- putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
253
- putString(MyConnectionService.EXTRA_DISPLAY_NAME, targetName)
254
- putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, callType == "Video")
255
- metadata?.let { putString("metadata", it) }
256
- }
449
+ activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
450
+ currentCallId = callId
451
+ Log.d(TAG, "Call $callId set to ACTIVE state")
257
452
 
258
- val extras = Bundle().apply {
259
- putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
260
- putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
261
- putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, callType == "Video")
262
- }
453
+ stopRingtone()
454
+ stopRingback()
455
+ cancelIncomingCallUI()
263
456
 
264
- try {
265
- telecomManager.placeCall(addressUri, extras)
266
- startForegroundService()
267
- startRingback()
268
- bringAppToForeground()
269
- keepScreenAwake(true)
270
- Log.d(TAG, "Successfully reported outgoing call to TelecomManager")
271
- } catch (e: Exception) {
272
- Log.e(TAG, "Failed to start outgoing call: ${e.message}", e)
273
- endCallInternal(callId)
457
+ if (!canMakeMultipleCalls) {
458
+ activeCalls.filter { it.key != callId }.values.forEach { otherCall ->
459
+ if (otherCall.state == CallState.ACTIVE) {
460
+ holdCallInternal(otherCall.callId, heldBySystem = false)
274
461
  }
275
- updateLockScreenBypass()
462
+ }
276
463
  }
277
464
 
278
- fun startCall(
279
- callId: String,
280
- callType: String,
281
- targetName: String,
282
- metadata: String? = null
283
- ) {
284
- Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
285
- metadata?.let { callMetadata[callId] = it }
286
-
287
- if (activeCalls.containsKey(callId)) {
288
- Log.w(TAG, "Call $callId already exists, cannot start again")
289
- return
290
- }
465
+ bringAppToForeground()
466
+ startForegroundService()
467
+ keepScreenAwake(true)
468
+ updateLockScreenBypass()
291
469
 
292
- activeCalls[callId] =
293
- CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
294
- currentCallId = callId
295
- Log.d(TAG, "Call $callId started as ACTIVE")
470
+ setAudioMode()
296
471
 
297
- registerPhoneAccount()
298
- bringAppToForeground()
299
- startForegroundService()
300
- keepScreenAwake(true)
472
+ // NEW: Improved initial audio route with longer delay and retry mechanism
473
+ mainHandler.postDelayed({
474
+ setInitialAudioRoute(callInfo.callType, isCallStart = true)
301
475
 
302
- synchronized(audioStateLock) {
303
- userSelectedAudioRoute = null
304
- }
476
+ // Retry after additional delay if needed for video calls
477
+ if (callInfo.callType == "Video") {
305
478
  mainHandler.postDelayed({
306
- updateAndApplyAudioRoute()
307
- }, 300)
308
-
309
- emitEvent(CallEventType.OUTGOING_CALL_ANSWERED, JSONObject().apply {
310
- put("callId", callId)
311
- put("callType", callType)
312
- put("displayName", targetName)
313
- })
314
- updateLockScreenBypass()
315
- }
316
-
317
- fun callAnsweredFromJS(callId: String) {
318
- Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
319
- coreCallAnswered(callId, isLocalAnswer = false)
320
- }
321
-
322
- fun answerCall(callId: String) {
323
- Log.d(TAG, "answerCall: $callId - local party answering")
324
- coreCallAnswered(callId, isLocalAnswer = true)
479
+ val currentRoute = getCurrentAudioRoute()
480
+ if (currentRoute != "Speaker") {
481
+ Log.d(TAG, "Retrying audio route for video call - current: $currentRoute")
482
+ setAudioRoute("Speaker")
483
+ }
484
+ }, 1000L)
485
+ }
486
+ }, 800L)
487
+
488
+ if (isLocalAnswer) {
489
+ emitCallAnsweredWithMetadata(callId)
490
+ } else {
491
+ emitOutgoingCallAnsweredWithMetadata(callId)
325
492
  }
326
493
 
327
- private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
328
- val callInfo = activeCalls[callId] ?: return
329
- activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
330
- currentCallId = callId
494
+ Log.d(TAG, "Call $callId successfully answered")
495
+ }
331
496
 
332
- stopRingtone()
333
- stopRingback()
334
- cancelIncomingCallUI()
497
+ private fun emitCallAnsweredWithMetadata(callId: String) {
498
+ val callInfo = activeCalls[callId] ?: return
499
+ val metadata = callMetadata[callId]
335
500
 
336
- bringAppToForeground()
337
- startForegroundService()
338
- keepScreenAwake(true)
339
-
340
- synchronized(audioStateLock) {
341
- userSelectedAudioRoute = null
501
+ emitEvent(CallEventType.CALL_ANSWERED, JSONObject().apply {
502
+ put("callId", callId)
503
+ put("callType", callInfo.callType)
504
+ put("displayName", callInfo.displayName)
505
+ callInfo.pictureUrl?.let { put("pictureUrl", it) }
506
+ metadata?.let {
507
+ try {
508
+ put("metadata", JSONObject(it))
509
+ } catch (e: Exception) {
510
+ put("metadata", it)
342
511
  }
343
- mainHandler.postDelayed({
344
- updateAndApplyAudioRoute()
345
- }, 300)
346
-
347
- if (isLocalAnswer) {
348
- emitCallAnsweredWithMetadata(callId)
512
+ }
513
+ })
514
+ }
515
+
516
+ private fun emitOutgoingCallAnsweredWithMetadata(callId: String) {
517
+ val callInfo = activeCalls[callId] ?: return
518
+ val metadata = callMetadata[callId]
519
+
520
+ emitEvent(CallEventType.OUTGOING_CALL_ANSWERED, JSONObject().apply {
521
+ put("callId", callId)
522
+ put("callType", callInfo.callType)
523
+ put("displayName", callInfo.displayName)
524
+ callInfo.pictureUrl?.let { put("pictureUrl", it) }
525
+ metadata?.let {
526
+ try {
527
+ put("metadata", JSONObject(it))
528
+ } catch (e: Exception) {
529
+ put("metadata", it)
349
530
  }
350
- updateLockScreenBypass()
531
+ }
532
+ })
533
+ }
534
+
535
+ fun holdCall(callId: String) {
536
+ holdCallInternal(callId, heldBySystem = false)
537
+ }
538
+
539
+ fun setOnHold(callId: String, onHold: Boolean) {
540
+ Log.d(TAG, "setOnHold: $callId, onHold: $onHold")
541
+ val callInfo = activeCalls[callId]
542
+ if (callInfo == null) {
543
+ Log.w(TAG, "Cannot set hold state for call $callId - not found")
544
+ return
351
545
  }
352
546
 
353
- private fun emitCallAnsweredWithMetadata(callId: String) {
354
- val callInfo = activeCalls[callId] ?: return
355
- emitEvent(CallEventType.CALL_ANSWERED, JSONObject().apply {
356
- put("callId", callId)
357
- put("callType", callInfo.callType)
358
- put("displayName", callInfo.displayName)
359
- callInfo.pictureUrl?.let { put("pictureUrl", it) }
360
- })
547
+ if (onHold && callInfo.state == CallState.ACTIVE) {
548
+ holdCallInternal(callId, heldBySystem = false)
549
+ } else if (!onHold && callInfo.state == CallState.HELD) {
550
+ unholdCallInternal(callId, resumedBySystem = false)
361
551
  }
362
-
363
- fun setOnHold(callId: String, onHold: Boolean) {
364
- if (onHold) {
365
- holdCall(callId)
366
- } else {
367
- unholdCall(callId)
368
- }
552
+ }
553
+
554
+ private fun holdCallInternal(callId: String, heldBySystem: Boolean) {
555
+ Log.d(TAG, "holdCallInternal: $callId, heldBySystem: $heldBySystem")
556
+ val callInfo = activeCalls[callId]
557
+ if (callInfo?.state != CallState.ACTIVE) {
558
+ Log.w(TAG, "Cannot hold call $callId - not in active state")
559
+ return
369
560
  }
370
561
 
371
- fun holdCall(callId: String) {
372
- val callInfo = activeCalls[callId]
373
- if (callInfo?.state != CallState.ACTIVE) {
374
- Log.w(TAG, "Cannot hold call $callId - not in active state")
375
- return
376
- }
377
- activeCalls[callId] = callInfo.copy(state = CallState.HELD)
378
- telecomConnections[callId]?.setOnHold()
379
- updateForegroundNotification()
380
- emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
381
- updateLockScreenBypass()
562
+ activeCalls[callId] = callInfo.copy(
563
+ state = CallState.HELD,
564
+ wasHeldBySystem = heldBySystem
565
+ )
566
+
567
+ telecomConnections[callId]?.setOnHold()
568
+ updateForegroundNotification()
569
+ emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
570
+ updateLockScreenBypass()
571
+ }
572
+
573
+ fun unholdCall(callId: String) {
574
+ unholdCallInternal(callId, resumedBySystem = false)
575
+ }
576
+
577
+ private fun unholdCallInternal(callId: String, resumedBySystem: Boolean) {
578
+ Log.d(TAG, "unholdCallInternal: $callId, resumedBySystem: $resumedBySystem")
579
+ val callInfo = activeCalls[callId]
580
+ if (callInfo?.state != CallState.HELD) {
581
+ Log.w(TAG, "Cannot unhold call $callId - not in held state")
582
+ return
382
583
  }
383
584
 
384
- fun unholdCall(callId: String) {
385
- val callInfo = activeCalls[callId]
386
- if (callInfo?.state != CallState.HELD) {
387
- Log.w(TAG, "Cannot unhold call $callId - not in held state")
388
- return
389
- }
390
- activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
391
- telecomConnections[callId]?.setActive()
392
- updateForegroundNotification()
393
- emitEvent(CallEventType.CALL_UNHELD, JSONObject().put("callId", callId))
394
- updateLockScreenBypass()
585
+ activeCalls[callId] = callInfo.copy(
586
+ state = CallState.ACTIVE,
587
+ wasHeldBySystem = false
588
+ )
589
+
590
+ telecomConnections[callId]?.setActive()
591
+ updateForegroundNotification()
592
+ emitEvent(CallEventType.CALL_UNHELD, JSONObject().put("callId", callId))
593
+ updateLockScreenBypass()
594
+ }
595
+
596
+ fun muteCall(callId: String) {
597
+ setMutedInternal(callId, true)
598
+ }
599
+
600
+ fun unmuteCall(callId: String) {
601
+ setMutedInternal(callId, false)
602
+ }
603
+
604
+ fun setMuted(callId: String, muted: Boolean) {
605
+ setMutedInternal(callId, muted)
606
+ }
607
+
608
+ private fun setMutedInternal(callId: String, muted: Boolean) {
609
+ val callInfo = activeCalls[callId]
610
+ if (callInfo == null) {
611
+ Log.w(TAG, "Cannot set mute state for call $callId - not found")
612
+ return
395
613
  }
396
614
 
397
- fun muteCall(callId: String) {
398
- setMuted(callId, true)
399
- }
615
+ val context = requireContext()
616
+ audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
400
617
 
401
- fun unmuteCall(callId: String) {
402
- setMuted(callId, false)
403
- }
618
+ val wasMuted = audioManager?.isMicrophoneMute ?: false
619
+ audioManager?.isMicrophoneMute = muted
404
620
 
405
- fun setMuted(callId: String, muted: Boolean) {
406
- val callInfo = activeCalls[callId]
407
- if (callInfo == null) {
408
- Log.w(TAG, "Cannot set mute state for call $callId - not found")
409
- return
410
- }
411
- val am = audioManager ?: return
412
- am.isMicrophoneMute = muted
413
- val eventType = if (muted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
414
- emitEvent(eventType, JSONObject().put("callId", callId))
621
+ if (wasMuted != muted) {
622
+ val eventType = if (muted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
623
+ emitEvent(eventType, JSONObject().put("callId", callId))
624
+ Log.d(TAG, "Call $callId mute state changed to: $muted")
415
625
  }
626
+ }
627
+
628
+ fun endCall(callId: String) {
629
+ Log.d(TAG, "endCall: $callId")
630
+ endCallInternal(callId)
631
+ }
632
+
633
+ fun endAllCalls() {
634
+ Log.d(TAG, "endAllCalls: Ending all active calls")
635
+ if (activeCalls.isEmpty()) return
416
636
 
417
- fun endCall(callId: String) {
418
- endCallInternal(callId)
637
+ activeCalls.keys.toList().forEach { callId ->
638
+ endCallInternal(callId)
419
639
  }
420
640
 
421
- fun endAllCalls() {
422
- Log.d(TAG, "endAllCalls: Ending all active calls")
423
- if (activeCalls.isEmpty()) return
641
+ activeCalls.clear()
642
+ telecomConnections.clear()
643
+ callMetadata.clear()
644
+ incomingCallIds.clear() // NEW: Clear duplicate tracking
645
+ currentCallId = null
424
646
 
425
- activeCalls.keys.toList().forEach { callId ->
426
- endCallInternal(callId)
427
- }
428
- updateLockScreenBypass()
647
+ cleanup()
648
+ updateLockScreenBypass()
649
+ }
650
+
651
+ private fun endCallInternal(callId: String) {
652
+ Log.d(TAG, "endCallInternal: $callId")
653
+
654
+ val callInfo = activeCalls[callId] ?: run {
655
+ Log.w(TAG, "Call $callId not found in active calls")
656
+ return
429
657
  }
430
658
 
431
- private fun endCallInternal(callId: String) {
432
- val callInfo = activeCalls.remove(callId) ?: return
433
- incomingCallIds.remove(callId)
434
- callMetadata.remove(callId)
659
+ val metadata = callMetadata.remove(callId)
660
+ activeCalls.remove(callId)
661
+ incomingCallIds.remove(callId) // NEW: Clean up duplicate tracking
435
662
 
436
- stopRingtone()
437
- stopRingback()
438
- cancelIncomingCallUI()
663
+ stopRingback()
664
+ stopRingtone()
665
+ cancelIncomingCallUI()
439
666
 
440
- telecomConnections.remove(callId)?.let {
441
- it.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
442
- it.destroy()
443
- }
667
+ if (currentCallId == callId) {
668
+ currentCallId =
669
+ activeCalls.filter { it.value.state != CallState.ENDED }.keys.firstOrNull()
670
+ }
444
671
 
445
- if (activeCalls.isEmpty()) {
446
- cleanup()
447
- } else {
448
- updateForegroundNotification()
449
- }
672
+ val context = requireContext()
673
+ val closeActivityIntent = Intent("com.qusaieilouti99.callmanager.CLOSE_CALL_ACTIVITY")
674
+ .setPackage(context.packageName)
675
+ .putExtra("callId", callId)
450
676
 
451
- callEndListeners.forEach { it.onCallEnded(callId) }
452
- emitEvent(CallEventType.CALL_ENDED, JSONObject().put("callId", callId))
453
- updateLockScreenBypass()
677
+ try {
678
+ context.sendBroadcast(closeActivityIntent)
679
+ Log.d(TAG, "Sent close broadcast for CallActivity: $callId")
680
+ } catch (e: Exception) {
681
+ Log.w(TAG, "Failed to send close broadcast: ${e.message}")
454
682
  }
455
683
 
456
- fun isCallActive(): Boolean = activeCalls.any {
457
- it.value.state == CallState.ACTIVE ||
458
- it.value.state == CallState.INCOMING ||
459
- it.value.state == CallState.DIALING ||
460
- it.value.state == CallState.HELD
684
+ telecomConnections[callId]?.let { connection ->
685
+ connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
686
+ connection.destroy()
687
+ removeTelecomConnection(callId)
461
688
  }
462
689
 
463
- fun setAudioRoute(route: String) {
464
- synchronized(audioStateLock) {
465
- Log.d(TAG, "User requested audio route: $route")
466
- userSelectedAudioRoute = route
467
- }
468
- updateAndApplyAudioRoute()
690
+ if (activeCalls.isEmpty()) {
691
+ cleanup()
692
+ } else {
693
+ updateForegroundNotification()
469
694
  }
470
695
 
471
- private fun updateAndApplyAudioRoute() {
472
- // *** CRITICAL FIX ***
473
- // Check for null currentCallId *before* using it as a key.
474
- val callId = currentCallId
475
- if (callId == null) {
476
- Log.d(TAG, "Skipping audio route update: currentCallId is null.")
477
- return
696
+ updateLockScreenBypass()
697
+
698
+ for (listener in callEndListeners) {
699
+ mainHandler.post {
700
+ try {
701
+ listener.onCallEnded(callId)
702
+ } catch (_: Throwable) {
703
+ // swallow
478
704
  }
705
+ }
706
+ }
479
707
 
480
- val call = activeCalls[callId]
481
- if (call == null || call.state != CallState.ACTIVE) {
482
- Log.d(TAG, "Skipping audio route update: No active call.")
483
- return
708
+ emitEvent(CallEventType.CALL_ENDED, JSONObject().apply {
709
+ put("callId", callId)
710
+ metadata?.let {
711
+ try { put("metadata", JSONObject(it)) }
712
+ catch (e: Exception) { put("metadata", it) }
713
+ }
714
+ })
715
+ }
716
+
717
+ fun getAudioDevices(): AudioRoutesInfo {
718
+ val context = requireContext()
719
+ audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
720
+ ?: return AudioRoutesInfo(emptyArray(), "Unknown")
721
+
722
+ val devices = mutableSetOf<String>()
723
+ devices.add("Speaker")
724
+ devices.add("Earpiece")
725
+
726
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
727
+ val infos = audioManager?.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
728
+ infos?.forEach { d ->
729
+ when (d.type) {
730
+ AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
731
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add("Bluetooth")
732
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
733
+ AudioDeviceInfo.TYPE_WIRED_HEADSET -> devices.add("Headset")
484
734
  }
735
+ }
736
+ } else {
737
+ @Suppress("DEPRECATION")
738
+ if (audioManager?.isBluetoothA2dpOn == true || audioManager?.isBluetoothScoOn == true)
739
+ devices.add("Bluetooth")
740
+ @Suppress("DEPRECATION")
741
+ if (audioManager?.isWiredHeadsetOn == true) devices.add("Headset")
742
+ }
485
743
 
486
- synchronized(audioStateLock) {
487
- val am = audioManager ?: return
488
- val availableDevices = getAvailableAudioDevices()
489
-
490
- val targetRoute = if (userSelectedAudioRoute != null && availableDevices.contains(userSelectedAudioRoute)) {
491
- userSelectedAudioRoute!!
492
- } else {
493
- when {
494
- availableDevices.contains("Bluetooth") -> "Bluetooth"
495
- availableDevices.contains("Headset") -> "Headset"
496
- call.callType == "Video" -> "Speaker"
497
- else -> "Earpiece"
498
- }
499
- }
744
+ val current = getCurrentAudioRoute()
745
+ Log.d(TAG, "Available audio devices: ${devices.toList()}, current: $current")
500
746
 
501
- Log.d(TAG, "Updating audio route. Available: $availableDevices, User Pref: $userSelectedAudioRoute, Target: $targetRoute")
502
-
503
- am.mode = AudioManager.MODE_IN_COMMUNICATION
504
-
505
- when (targetRoute) {
506
- "Bluetooth" -> {
507
- am.isSpeakerphoneOn = false
508
- am.startBluetoothSco()
509
- am.isBluetoothScoOn = true
510
- }
511
- "Headset" -> {
512
- am.isSpeakerphoneOn = false
513
- am.stopBluetoothSco()
514
- am.isBluetoothScoOn = false
515
- }
516
- "Speaker" -> {
517
- am.isSpeakerphoneOn = true
518
- am.stopBluetoothSco()
519
- am.isBluetoothScoOn = false
520
- }
521
- "Earpiece" -> {
522
- am.isSpeakerphoneOn = false
523
- am.stopBluetoothSco()
524
- am.isBluetoothScoOn = false
525
- }
526
- }
747
+ // Convert strings to StringHolder objects
748
+ val deviceHolders = devices.map { StringHolder(it) }.toTypedArray()
749
+ lastAudioRoutesInfo = AudioRoutesInfo(deviceHolders, current)
750
+ return AudioRoutesInfo(deviceHolders, current)
751
+ }
527
752
 
528
- mainHandler.postDelayed({ emitAudioRouteChanged() }, 100)
529
- }
753
+ fun setAudioRoute(route: String) {
754
+ Log.d(TAG, "setAudioRoute called: $route")
755
+
756
+ val ctx = requireContext()
757
+ if (audioManager == null) {
758
+ audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
530
759
  }
760
+ val am = audioManager!!
531
761
 
532
- private fun getAvailableAudioDevices(): Set<String> {
533
- val am = audioManager ?: return emptySet()
534
- val devices = mutableSetOf<String>()
535
- devices.add("Earpiece")
536
- devices.add("Speaker")
762
+ if (am.mode != AudioManager.MODE_IN_COMMUNICATION) {
763
+ am.mode = AudioManager.MODE_IN_COMMUNICATION
764
+ }
537
765
 
766
+ when (route) {
767
+ "Speaker" -> {
768
+ am.isSpeakerphoneOn = true
769
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
770
+ am.stopBluetoothSco()
771
+ am.isBluetoothScoOn = false
772
+ }
773
+ Log.d(TAG, "Audio routed to SPEAKER")
774
+ }
775
+ "Earpiece" -> {
776
+ am.isSpeakerphoneOn = false
777
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
778
+ am.stopBluetoothSco()
779
+ am.isBluetoothScoOn = false
780
+ }
781
+ Log.d(TAG, "Audio routed to EARPIECE")
782
+ }
783
+ "Bluetooth" -> {
784
+ am.isSpeakerphoneOn = false
538
785
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
539
- val audioDevices = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
540
- audioDevices.forEach {
541
- when (it.type) {
542
- AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> devices.add("Bluetooth")
543
- AudioDeviceInfo.TYPE_WIRED_HEADSET, AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> devices.add("Headset")
544
- }
545
- }
786
+ am.startBluetoothSco()
787
+ am.isBluetoothScoOn = true
788
+ Log.d(TAG, "Audio routed to BLUETOOTH")
546
789
  } else {
547
- @Suppress("DEPRECATION")
548
- if (am.isBluetoothScoAvailableOffCall) devices.add("Bluetooth")
549
- @Suppress("DEPRECATION")
550
- if (am.isWiredHeadsetOn) devices.add("Headset")
790
+ Log.w(TAG, "Bluetooth SCO not supported on this OS version")
551
791
  }
552
- return devices
792
+ }
793
+ "Headset" -> {
794
+ am.isSpeakerphoneOn = false
795
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
796
+ am.stopBluetoothSco()
797
+ am.isBluetoothScoOn = false
798
+ }
799
+ Log.d(TAG, "Audio routed to HEADSET")
800
+ }
801
+ else -> {
802
+ Log.w(TAG, "Unknown audio route: $route")
803
+ return
804
+ }
553
805
  }
554
-
555
- fun getAudioDevices(): AudioRoutesInfo {
556
- val available = getAvailableAudioDevices().map { StringHolder(it) }.toTypedArray()
557
- val current = getCurrentAudioRoute()
558
- return AudioRoutesInfo(available, current)
806
+ emitAudioRouteChanged()
807
+ }
808
+
809
+ private fun getCurrentAudioRoute(): String {
810
+ return when {
811
+ audioManager?.isBluetoothScoOn == true -> "Bluetooth"
812
+ audioManager?.isSpeakerphoneOn == true -> "Speaker"
813
+ audioManager?.isWiredHeadsetOn == true -> "Headset"
814
+ else -> "Earpiece"
559
815
  }
560
-
561
- private fun getCurrentAudioRoute(): String {
562
- val am = audioManager ?: return "Unknown"
563
- return when {
564
- am.isBluetoothScoOn -> "Bluetooth"
565
- am.isWiredHeadsetOn -> "Headset"
566
- am.isSpeakerphoneOn -> "Speaker"
567
- else -> "Earpiece"
568
- }
816
+ }
817
+
818
+ // NEW: Improved initial audio route setting
819
+ private fun setInitialAudioRoute(callType: String, isCallStart: Boolean = false) {
820
+ val avail = getAudioDevices()
821
+ // Extract string values for comparison
822
+ val deviceStrings = avail.devices.map { it.value }
823
+
824
+ val defaultRoute = when {
825
+ deviceStrings.contains("Bluetooth") -> "Bluetooth"
826
+ deviceStrings.contains("Headset") -> "Headset"
827
+ callType == "Video" -> "Speaker"
828
+ else -> "Earpiece"
569
829
  }
570
830
 
571
- private fun emitAudioRouteChanged() {
572
- val info = getAudioDevices()
573
- val payload = JSONObject().apply {
574
- put("devices", JSONArray(info.devices.map { it.value }))
575
- put("currentRoute", info.currentRoute)
576
- }
577
- emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, payload)
578
- Log.d(TAG, "Emitted AUDIO_ROUTE_CHANGED: Current='${info.currentRoute}', Available=${info.devices.map { it.value }}")
579
- }
831
+ Log.d(TAG, "Setting initial audio route: $defaultRoute for call type: $callType (isCallStart: $isCallStart)")
580
832
 
581
- private val audioDeviceCallback = object : AudioDeviceCallback() {
582
- override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
583
- Log.d(TAG, "Audio devices added. Triggering audio route update.")
584
- synchronized(audioStateLock) {
585
- userSelectedAudioRoute = null
586
- }
587
- updateAndApplyAudioRoute()
588
- }
589
- override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
590
- Log.d(TAG, "Audio devices removed. Triggering audio route update.")
591
- synchronized(audioStateLock) {
592
- val removedDeviceTypes = removedDevices.map {
593
- when(it.type) {
594
- AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "Bluetooth"
595
- AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Headset"
596
- else -> ""
597
- }
598
- }
599
- if (removedDeviceTypes.contains(userSelectedAudioRoute)) {
600
- userSelectedAudioRoute = null
601
- }
833
+ // For call start, ensure audio mode is properly set first
834
+ if (isCallStart) {
835
+ setAudioMode()
836
+
837
+ // Additional delay for audio system to be ready
838
+ if (callType == "Video") {
839
+ mainHandler.postDelayed({
840
+ setAudioRoute(defaultRoute)
841
+
842
+ // Force speaker for video calls with additional verification
843
+ mainHandler.postDelayed({
844
+ val currentRoute = getCurrentAudioRoute()
845
+ if (currentRoute != "Speaker" && !deviceStrings.contains("Bluetooth") && !deviceStrings.contains("Headset")) {
846
+ Log.d(TAG, "Forcing speaker for video call - current route was: $currentRoute")
847
+ setAudioRoute("Speaker")
602
848
  }
603
- updateAndApplyAudioRoute()
604
- }
849
+ }, 300L)
850
+ }, 200L)
851
+ } else {
852
+ setAudioRoute(defaultRoute)
853
+ }
854
+ } else {
855
+ setAudioRoute(defaultRoute)
605
856
  }
606
-
607
- private fun registerAudioDeviceCallback() {
608
- audioManager?.registerAudioDeviceCallback(audioDeviceCallback, mainHandler)
857
+ }
858
+
859
+ private fun setAudioMode() {
860
+ audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
861
+ Log.d(TAG, "Audio mode set to MODE_IN_COMMUNICATION")
862
+ }
863
+
864
+ private fun resetAudioMode() {
865
+ if (activeCalls.isEmpty()) {
866
+ audioManager?.mode = AudioManager.MODE_NORMAL
867
+ audioManager?.stopBluetoothSco()
868
+ audioManager?.isBluetoothScoOn = false
869
+ audioManager?.isSpeakerphoneOn = false
870
+ Log.d(TAG, "Audio mode reset to MODE_NORMAL")
609
871
  }
610
-
611
- private fun cleanup() {
612
- Log.d(TAG, "Performing cleanup")
613
- stopForegroundService()
614
- keepScreenAwake(false)
615
- synchronized(audioStateLock) {
616
- userSelectedAudioRoute = null
617
- }
618
- audioManager?.mode = AudioManager.MODE_NORMAL
619
- audioManager?.isSpeakerphoneOn = false
620
- audioManager?.stopBluetoothSco()
872
+ }
873
+
874
+ private fun emitAudioRouteChanged() {
875
+ val info = getAudioDevices()
876
+ // Extract string values from StringHolder objects
877
+ val deviceStrings = info.devices.map { it.value }
878
+ val payload = JSONObject().apply {
879
+ put("devices", JSONArray(deviceStrings))
880
+ put("currentRoute", info.currentRoute)
621
881
  }
622
-
623
- private fun createNotificationChannel() {
624
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
625
- val context = requireContext()
626
- val channel = NotificationChannel(
627
- NOTIF_CHANNEL_ID,
628
- "Incoming Call Channel",
629
- NotificationManager.IMPORTANCE_HIGH
630
- ).apply {
631
- description = "Notifications for incoming calls"
632
- setBypassDnd(true)
633
- lockscreenVisibility = Notification.VISIBILITY_PUBLIC
634
- setSound(null, null)
635
- }
636
- val manager = context.getSystemService(NotificationManager::class.java)
637
- manager.createNotificationChannel(channel)
638
- }
882
+ emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, payload)
883
+ Log.d(TAG, "Audio route changed: ${info.currentRoute}, available: $deviceStrings")
884
+ }
885
+
886
+ private val audioDeviceCallback = object : AudioDeviceCallback() {
887
+ override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
888
+ Log.d(TAG, "Audio devices added")
889
+ emitAudioDevicesChanged()
639
890
  }
640
-
641
- private fun showIncomingCallUI(callId: String, callerName: String, callType: String, callerPicUrl: String?) {
642
- val context = requireContext()
643
- Log.d(TAG, "Showing incoming call UI for $callId")
644
- createNotificationChannel()
645
-
646
- showCallActivityOverlay(context, callId, callerName, callType, callerPicUrl)
647
- showStandardNotification(context, callId, callerName, callType, callerPicUrl)
648
-
649
- playRingtone()
891
+ override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
892
+ Log.d(TAG, "Audio devices removed")
893
+ emitAudioDevicesChanged()
650
894
  }
651
-
652
- private fun showCallActivityOverlay(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
653
- val overlayIntent = Intent(context, CallActivity::class.java).apply {
654
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
655
- putExtra("callId", callId)
656
- putExtra("callerName", callerName)
657
- putExtra("callType", callType)
658
- callerPicUrl?.let { putExtra("callerAvatar", it) }
659
- }
660
- try {
661
- context.startActivity(overlayIntent)
662
- } catch (e: Exception) {
663
- Log.e(TAG, "Failed to launch CallActivity overlay: ${e.message}")
664
- }
895
+ }
896
+
897
+ private fun emitAudioDevicesChanged() {
898
+ val info = getAudioDevices()
899
+ // Extract string values from StringHolder objects
900
+ val deviceStrings = info.devices.map { it.value }
901
+ val payload = JSONObject().apply {
902
+ put("devices", JSONArray(deviceStrings))
903
+ put("currentRoute", info.currentRoute)
665
904
  }
666
-
667
- private fun showStandardNotification(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
668
- val fullScreenIntent = Intent(context, CallActivity::class.java).apply {
669
- putExtra("callId", callId)
670
- }
671
- val fullScreenPendingIntent = PendingIntent.getActivity(
672
- context, callId.hashCode(), fullScreenIntent,
673
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
905
+ emitEvent(CallEventType.AUDIO_DEVICES_CHANGED, payload)
906
+ Log.d(TAG, "Audio devices changed: available: $deviceStrings")
907
+ }
908
+
909
+ fun registerAudioDeviceCallback() {
910
+ val context = requireContext()
911
+ audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
912
+ audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
913
+ }
914
+
915
+ fun unregisterAudioDeviceCallback() {
916
+ val context = requireContext()
917
+ audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
918
+ audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
919
+ }
920
+
921
+ fun keepScreenAwake(keepAwake: Boolean) {
922
+ val context = requireContext()
923
+ val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
924
+ if (keepAwake) {
925
+ if (wakeLock == null || wakeLock!!.isHeld.not()) {
926
+ wakeLock = powerManager.newWakeLock(
927
+ PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
928
+ "CallEngine:WakeLock"
674
929
  )
675
-
676
- val builder = Notification.Builder(context, NOTIF_CHANNEL_ID)
677
- .setSmallIcon(android.R.drawable.sym_call_incoming)
678
- .setContentTitle("Incoming Call")
679
- .setContentText(callerName)
680
- .setPriority(Notification.PRIORITY_MAX)
681
- .setCategory(Notification.CATEGORY_CALL)
682
- .setFullScreenIntent(fullScreenPendingIntent, true)
683
- .setOngoing(true)
684
-
685
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && supportsCallStyleNotifications()) {
686
- val person = android.app.Person.Builder().setName(callerName).build()
687
- val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply { action = "com.qusaieilouti99.callmanager.ANSWER_CALL"; putExtra("callId", callId) }
688
- val declineIntent = Intent(context, CallNotificationActionReceiver::class.java).apply { action = "com.qusaieilouti99.callmanager.DECLINE_CALL"; putExtra("callId", callId) }
689
- val answerPI = PendingIntent.getBroadcast(context, callId.hashCode() + 1, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
690
- val declinePI = PendingIntent.getBroadcast(context, callId.hashCode() + 2, declineIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
691
- builder.setStyle(Notification.CallStyle.forIncomingCall(person, declinePI, answerPI))
930
+ wakeLock?.acquire(10 * 60 * 1000L)
931
+ Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK")
932
+ }
933
+ } else {
934
+ wakeLock?.let {
935
+ if (it.isHeld) {
936
+ it.release()
937
+ Log.d(TAG, "Released SCREEN_DIM_WAKE_LOCK")
692
938
  }
693
-
694
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
695
- notificationManager.notify(NOTIF_ID, builder.build())
939
+ }
940
+ wakeLock = null
696
941
  }
697
-
698
- fun cancelIncomingCallUI() {
699
- val context = requireContext()
700
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
701
- notificationManager.cancel(NOTIF_ID)
702
- stopRingtone()
942
+ }
943
+
944
+ fun getActiveCalls(): List<CallInfo> = activeCalls.values.toList()
945
+ fun getCurrentCallId(): String? = currentCallId
946
+ fun isCallActive(): Boolean = activeCalls.any {
947
+ it.value.state == CallState.ACTIVE ||
948
+ it.value.state == CallState.INCOMING ||
949
+ it.value.state == CallState.DIALING ||
950
+ it.value.state == CallState.HELD
951
+ }
952
+
953
+ private fun validateOutgoingCallRequest(): Boolean {
954
+ return !activeCalls.any {
955
+ it.value.state == CallState.INCOMING || it.value.state == CallState.ACTIVE
703
956
  }
957
+ }
958
+
959
+ private fun rejectIncomingCallCollision(callId: String, reason: String) {
960
+ callMetadata.remove(callId)
961
+ incomingCallIds.remove(callId) // NEW: Clean up duplicate tracking
962
+ emitEvent(CallEventType.CALL_REJECTED, JSONObject().apply {
963
+ put("callId", callId)
964
+ put("reason", reason)
965
+ })
966
+ }
967
+
968
+ private fun createNotificationChannel() {
969
+ val context = requireContext()
970
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
971
+ val channel = NotificationChannel(
972
+ NOTIF_CHANNEL_ID,
973
+ "Incoming Call Channel",
974
+ NotificationManager.IMPORTANCE_HIGH
975
+ )
976
+ channel.description = "Notifications for incoming calls"
977
+ channel.enableLights(true)
978
+ channel.lightColor = Color.GREEN
979
+ channel.enableVibration(true)
980
+ channel.setBypassDnd(true)
981
+ channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
982
+
983
+ // NEW: Improved sound handling to prevent double ringing
984
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
985
+ channel.setSound(
986
+ RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE),
987
+ AudioAttributes.Builder()
988
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
989
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
990
+ .build()
991
+ )
992
+ } else {
993
+ // For API 31+, disable notification sound to prevent conflicts with custom ringtone
994
+ channel.setSound(null, null)
995
+ channel.importance = NotificationManager.IMPORTANCE_HIGH
996
+ }
997
+ val manager = context.getSystemService(NotificationManager::class.java)
998
+ manager.createNotificationChannel(channel)
999
+ }
1000
+ }
704
1001
 
705
- private fun playRingtone() {
706
- if (ringtone?.isPlaying == true) return
707
- val context = requireContext()
708
- audioManager?.mode = AudioManager.MODE_RINGTONE
1002
+ private fun showIncomingCallUI(callId: String, callerName: String, callType: String, callerPicUrl: String?) {
1003
+ val context = requireContext()
1004
+ Log.d(TAG, "Showing incoming call UI for $callId")
709
1005
 
710
- vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
711
- val pattern = longArrayOf(0, 1000, 500)
712
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
713
- vibrator?.vibrate(VibrationEffect.createWaveform(pattern, 0))
714
- } else {
715
- @Suppress("DEPRECATION")
716
- vibrator?.vibrate(pattern, 0)
717
- }
1006
+ val useCallStyleNotification = supportsCallStyleNotifications()
1007
+ Log.d(TAG, "Using CallStyle notification: $useCallStyleNotification")
718
1008
 
719
- try {
720
- val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
721
- ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
722
- ringtone?.play()
723
- Log.d(TAG, "Manual ringtone started.")
724
- } catch (e: Exception) {
725
- Log.e(TAG, "Failed to play ringtone", e)
726
- }
1009
+ if (isDeviceLocked(context) || !useCallStyleNotification) {
1010
+ Log.d(TAG, "Device is locked or CallStyle not supported - using overlay/fallback approach")
1011
+ showCallActivityOverlay(context, callId, callerName, callType, callerPicUrl)
1012
+ } else {
1013
+ Log.d(TAG, "Device is unlocked and supports CallStyle - using enhanced notification")
1014
+ showStandardNotification(context, callId, callerName, callType, callerPicUrl)
727
1015
  }
728
1016
 
729
- fun stopRingtone() {
730
- vibrator?.cancel()
731
- vibrator = null
732
- ringtone?.stop()
733
- ringtone = null
734
- Log.d(TAG, "Manual ringtone stopped.")
1017
+ // NEW: Improved ringtone handling to prevent double ringing
1018
+ playRingtone()
1019
+ }
1020
+
1021
+ private fun isDeviceLocked(context: Context): Boolean {
1022
+ val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
1023
+ return keyguardManager.isKeyguardLocked
1024
+ }
1025
+
1026
+ private fun showCallActivityOverlay(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
1027
+ val overlayIntent = Intent(context, CallActivity::class.java).apply {
1028
+ addFlags(
1029
+ Intent.FLAG_ACTIVITY_NEW_TASK or
1030
+ Intent.FLAG_ACTIVITY_CLEAR_TASK or
1031
+ Intent.FLAG_ACTIVITY_NO_ANIMATION or
1032
+ Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
1033
+ )
1034
+ putExtra("callId", callId)
1035
+ putExtra("callerName", callerName)
1036
+ putExtra("callType", callType)
1037
+ callerPicUrl?.let { putExtra("callerAvatar", it) }
1038
+ putExtra("LOCK_SCREEN_MODE", true)
735
1039
  }
736
1040
 
737
- private fun startRingback() {
738
- val context = requireContext()
739
- if (ringbackPlayer?.isPlaying == true) return
1041
+ try {
1042
+ val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
1043
+ val wakeLock = powerManager.newWakeLock(
1044
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
1045
+ "CallEngine:LockScreenWake"
1046
+ )
1047
+ wakeLock.acquire(5000)
1048
+ context.startActivity(overlayIntent)
1049
+ Log.d(TAG, "Successfully launched CallActivity overlay")
1050
+ } catch (e: Exception) {
1051
+ Log.e(TAG, "Overlay failed, falling back to standard notification: ${e.message}")
1052
+ showStandardNotification(context, callId, callerName, callType, callerPicUrl)
1053
+ }
1054
+ }
1055
+
1056
+ private fun showStandardNotification(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
1057
+ createNotificationChannel()
1058
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
1059
+
1060
+ val fullScreenIntent = Intent(context, CallActivity::class.java).apply {
1061
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
1062
+ putExtra("callId", callId)
1063
+ putExtra("callerName", callerName)
1064
+ putExtra("callType", callType)
1065
+ callerPicUrl?.let { putExtra("callerAvatar", it) }
1066
+ }
740
1067
 
741
- try {
742
- val ringbackUri =
743
- Uri.parse("android.resource://${context.packageName}/raw/ringback_tone")
744
- ringbackPlayer = MediaPlayer.create(context, ringbackUri)
745
- ringbackPlayer?.apply {
746
- isLooping = true
747
- start()
748
- }
749
- } catch (e: Exception) {
750
- Log.e(TAG, "Failed to play ringback tone: ${e.message}")
751
- }
1068
+ val fullScreenPendingIntent = PendingIntent.getActivity(
1069
+ context, callId.hashCode(), fullScreenIntent,
1070
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
1071
+ )
1072
+
1073
+ val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
1074
+ action = "com.qusaieilouti99.callmanager.ANSWER_CALL"
1075
+ putExtra("callId", callId)
1076
+ }
1077
+ val answerPendingIntent = PendingIntent.getBroadcast(
1078
+ context, 0, answerIntent,
1079
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
1080
+ )
1081
+
1082
+ val declineIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
1083
+ action = "com.qusaieilouti99.callmanager.DECLINE_CALL"
1084
+ putExtra("callId", callId)
1085
+ }
1086
+ val declinePendingIntent = PendingIntent.getBroadcast(
1087
+ context, 1, declineIntent,
1088
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
1089
+ )
1090
+
1091
+ val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && supportsCallStyleNotifications()) {
1092
+ val person = android.app.Person.Builder()
1093
+ .setName(callerName)
1094
+ .setImportant(true)
1095
+ .build()
1096
+ Notification.Builder(context, NOTIF_CHANNEL_ID)
1097
+ .setSmallIcon(android.R.drawable.sym_call_incoming)
1098
+ .setStyle(
1099
+ Notification.CallStyle.forIncomingCall(
1100
+ person,
1101
+ declinePendingIntent,
1102
+ answerPendingIntent
1103
+ )
1104
+ )
1105
+ .setFullScreenIntent(fullScreenPendingIntent, true)
1106
+ .setOngoing(true)
1107
+ .setAutoCancel(false)
1108
+ .setCategory(Notification.CATEGORY_CALL)
1109
+ .setPriority(Notification.PRIORITY_MAX)
1110
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
1111
+ .build()
1112
+ } else {
1113
+ Notification.Builder(context, NOTIF_CHANNEL_ID)
1114
+ .setSmallIcon(android.R.drawable.sym_call_incoming)
1115
+ .setContentTitle("Incoming Call")
1116
+ .setContentText(callerName)
1117
+ .setPriority(Notification.PRIORITY_MAX)
1118
+ .setCategory(Notification.CATEGORY_CALL)
1119
+ .setFullScreenIntent(fullScreenPendingIntent, true)
1120
+ .addAction(android.R.drawable.sym_action_call, "Answer", answerPendingIntent)
1121
+ .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Decline", declinePendingIntent)
1122
+ .setOngoing(true)
1123
+ .setAutoCancel(false)
1124
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
1125
+ .build()
752
1126
  }
753
1127
 
754
- private fun stopRingback() {
755
- try {
756
- ringbackPlayer?.stop()
757
- ringbackPlayer?.release()
758
- } catch (e: Exception) {
759
- Log.e(TAG, "Error stopping ringback: ${e.message}")
760
- } finally {
761
- ringbackPlayer = null
762
- }
1128
+ notificationManager.notify(NOTIF_ID, notification)
1129
+ }
1130
+
1131
+ fun cancelIncomingCallUI() {
1132
+ val context = requireContext()
1133
+ val notificationManager =
1134
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
1135
+ notificationManager.cancel(NOTIF_ID)
1136
+ stopRingtone()
1137
+ }
1138
+
1139
+ private fun startForegroundService() {
1140
+ val context = requireContext()
1141
+ val currentCall = activeCalls.values.find {
1142
+ it.state == CallState.ACTIVE ||
1143
+ it.state == CallState.INCOMING ||
1144
+ it.state == CallState.DIALING ||
1145
+ it.state == CallState.HELD
763
1146
  }
764
1147
 
765
- fun keepScreenAwake(keepAwake: Boolean) {
766
- val context = requireContext()
767
- val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
768
- if (keepAwake) {
769
- if (wakeLock == null || wakeLock!!.isHeld.not()) {
770
- wakeLock = powerManager.newWakeLock(
771
- PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
772
- "CallEngine:WakeLock"
773
- )
774
- wakeLock?.acquire(10 * 60 * 1000L)
775
- Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK")
776
- }
777
- } else {
778
- wakeLock?.let {
779
- if (it.isHeld) {
780
- it.release()
781
- Log.d(TAG, "Released SCREEN_DIM_WAKE_LOCK")
782
- }
783
- }
784
- wakeLock = null
785
- }
1148
+ val intent = Intent(context, CallForegroundService::class.java)
1149
+ currentCall?.let {
1150
+ intent.putExtra("callId", it.callId)
1151
+ intent.putExtra("callType", it.callType)
1152
+ intent.putExtra("displayName", it.displayName)
1153
+ intent.putExtra("state", it.state.name)
786
1154
  }
787
1155
 
788
- private fun bringAppToForeground() {
789
- val context = requireContext()
790
- val packageName = context.packageName
791
- val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
792
- launchIntent?.addFlags(
793
- Intent.FLAG_ACTIVITY_NEW_TASK or
794
- Intent.FLAG_ACTIVITY_CLEAR_TOP or
795
- Intent.FLAG_ACTIVITY_SINGLE_TOP
796
- )
797
- try {
798
- context.startActivity(launchIntent)
799
- Handler(Looper.getMainLooper()).postDelayed({
800
- updateLockScreenBypass()
801
- }, 100)
802
- } catch (e: Exception) {
803
- Log.e(TAG, "Failed to bring app to foreground: ${e.message}")
1156
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1157
+ context.startForegroundService(intent)
1158
+ } else {
1159
+ context.startService(intent)
1160
+ }
1161
+ }
1162
+
1163
+ private fun stopForegroundService() {
1164
+ val context = requireContext()
1165
+ val intent = Intent(context, CallForegroundService::class.java)
1166
+ context.stopService(intent)
1167
+ }
1168
+
1169
+ private fun updateForegroundNotification() {
1170
+ startForegroundService()
1171
+ }
1172
+
1173
+ private fun isMainActivityInForeground(): Boolean {
1174
+ val context = requireContext()
1175
+ val activityManager =
1176
+ context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
1177
+
1178
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1179
+ try {
1180
+ val tasks = activityManager.appTasks
1181
+ if (tasks.isNotEmpty()) {
1182
+ val taskInfo = tasks[0].taskInfo
1183
+ return taskInfo.topActivity?.className?.contains("MainActivity") == true
804
1184
  }
1185
+ } catch (e: Exception) {
1186
+ Log.w(TAG, "Failed to get app tasks: ${e.message}")
1187
+ }
1188
+ } else {
1189
+ try {
1190
+ @Suppress("DEPRECATION")
1191
+ val tasks = activityManager.getRunningTasks(1)
1192
+ if (tasks.isNotEmpty()) {
1193
+ val runningTaskInfo = tasks[0]
1194
+ return runningTaskInfo.topActivity?.className?.contains("MainActivity") == true
1195
+ }
1196
+ } catch (e: Exception) {
1197
+ Log.w(TAG, "Failed to get running tasks: ${e.message}")
1198
+ }
805
1199
  }
1200
+ return false
1201
+ }
806
1202
 
807
- private fun registerPhoneAccount() {
808
- val context = requireContext()
809
- val telecomManager =
810
- context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
811
- val phoneAccountHandle = getPhoneAccountHandle()
812
-
813
- if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
814
- val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
815
- .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
816
- .build()
1203
+ private fun bringAppToForeground() {
1204
+ if (isMainActivityInForeground()) {
1205
+ Log.d(TAG, "MainActivity is already in foreground, skipping")
1206
+ return
1207
+ }
817
1208
 
818
- try {
819
- telecomManager.registerPhoneAccount(phoneAccount)
820
- Log.d(TAG, "PhoneAccount registered successfully")
821
- } catch (e: Exception) {
822
- Log.e(TAG, "Failed to register PhoneAccount: ${e.message}", e)
823
- }
824
- }
1209
+ Log.d(TAG, "Bringing app to foreground")
1210
+ val context = requireContext()
1211
+ val packageName = context.packageName
1212
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
1213
+ launchIntent?.addFlags(
1214
+ Intent.FLAG_ACTIVITY_NEW_TASK or
1215
+ Intent.FLAG_ACTIVITY_CLEAR_TOP or
1216
+ Intent.FLAG_ACTIVITY_SINGLE_TOP
1217
+ )
1218
+
1219
+ if (isCallActive()) {
1220
+ launchIntent?.putExtra("BYPASS_LOCK_SCREEN", true)
825
1221
  }
826
1222
 
827
- private fun getPhoneAccountHandle(): PhoneAccountHandle {
828
- val context = requireContext()
829
- return PhoneAccountHandle(
830
- ComponentName(context, MyConnectionService::class.java),
831
- PHONE_ACCOUNT_ID
832
- )
1223
+ try {
1224
+ context.startActivity(launchIntent)
1225
+ Handler(Looper.getMainLooper()).postDelayed({
1226
+ updateLockScreenBypass()
1227
+ }, 100)
1228
+ } catch (e: Exception) {
1229
+ Log.e(TAG, "Failed to bring app to foreground: ${e.message}")
1230
+ }
1231
+ }
1232
+
1233
+ private fun registerPhoneAccount() {
1234
+ val context = requireContext()
1235
+ val telecomManager =
1236
+ context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
1237
+ val phoneAccountHandle = getPhoneAccountHandle()
1238
+
1239
+ if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
1240
+ val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
1241
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
1242
+ .build()
1243
+
1244
+ try {
1245
+ telecomManager.registerPhoneAccount(phoneAccount)
1246
+ Log.d(TAG, "PhoneAccount registered successfully")
1247
+ } catch (e: Exception) {
1248
+ Log.e(TAG, "Failed to register PhoneAccount: ${e.message}", e)
1249
+ }
833
1250
  }
1251
+ }
1252
+
1253
+ private fun getPhoneAccountHandle(): PhoneAccountHandle {
1254
+ val context = requireContext()
1255
+ return PhoneAccountHandle(
1256
+ ComponentName(context, MyConnectionService::class.java),
1257
+ PHONE_ACCOUNT_ID
1258
+ )
1259
+ }
1260
+
1261
+ // NEW: Improved ringtone handling to prevent double ringing
1262
+ private fun playRingtone() {
1263
+ synchronized(ringtoneStateLock) {
1264
+ if (isCustomRingtoneActive) {
1265
+ Log.d(TAG, "Custom ringtone already active, skipping")
1266
+ return
1267
+ }
1268
+
1269
+ val context = requireContext()
1270
+ audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
1271
+
1272
+ // Only set ringtone mode if not already in communication mode
1273
+ val currentMode = audioManager?.mode ?: AudioManager.MODE_NORMAL
1274
+ if (currentMode != AudioManager.MODE_IN_COMMUNICATION) {
1275
+ audioManager?.mode = AudioManager.MODE_RINGTONE
1276
+ }
834
1277
 
835
- private fun startForegroundService() {
836
- val context = requireContext()
837
- val currentCall = activeCalls.values.find {
838
- it.state == CallState.ACTIVE ||
839
- it.state == CallState.INCOMING ||
840
- it.state == CallState.DIALING ||
841
- it.state == CallState.HELD
1278
+ vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
1279
+ vibrator?.let { v ->
1280
+ val pattern = longArrayOf(0L, 500L, 500L)
1281
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1282
+ v.vibrate(VibrationEffect.createWaveform(pattern, 0))
1283
+ } else {
1284
+ @Suppress("DEPRECATION")
1285
+ v.vibrate(pattern, 0)
842
1286
  }
843
-
844
- val intent = Intent(context, CallForegroundService::class.java)
845
- currentCall?.let {
846
- intent.putExtra("callId", it.callId)
847
- intent.putExtra("callType", it.callType)
848
- intent.putExtra("displayName", it.displayName)
849
- intent.putExtra("state", it.state.name)
1287
+ }
1288
+
1289
+ try {
1290
+ // Check if system ringtone is already playing for this call (API 31+)
1291
+ val shouldPlayCustomRingtone = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1292
+ // For newer APIs, be more cautious about playing custom ringtone
1293
+ // if CallStyle notification is being used
1294
+ !supportsCallStyleNotifications()
1295
+ } else {
1296
+ true
850
1297
  }
851
1298
 
852
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
853
- context.startForegroundService(intent)
1299
+ if (shouldPlayCustomRingtone) {
1300
+ val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
1301
+ ringtone = RingtoneManager.getRingtone(context, uri)
1302
+ ringtone?.play()
1303
+ isCustomRingtoneActive = true
1304
+ Log.d(TAG, "Custom ringtone started playing")
854
1305
  } else {
855
- context.startService(intent)
1306
+ Log.d(TAG, "Skipping custom ringtone - system should handle it")
856
1307
  }
1308
+ } catch (e: Exception) {
1309
+ Log.e(TAG, "Failed to play ringtone", e)
1310
+ }
857
1311
  }
1312
+ }
858
1313
 
859
- private fun stopForegroundService() {
860
- val context = requireContext()
861
- val intent = Intent(context, CallForegroundService::class.java)
862
- context.stopService(intent)
1314
+ fun stopRingtone() {
1315
+ synchronized(ringtoneStateLock) {
1316
+ try {
1317
+ ringtone?.stop()
1318
+ isCustomRingtoneActive = false
1319
+ Log.d(TAG, "Ringtone stopped")
1320
+ } catch (e: Exception) {
1321
+ Log.e(TAG, "Error stopping ringtone", e)
1322
+ }
1323
+ ringtone = null
1324
+
1325
+ vibrator?.cancel()
1326
+ vibrator = null
863
1327
  }
864
-
865
- private fun updateForegroundNotification() {
866
- startForegroundService()
1328
+ }
1329
+
1330
+ private fun startRingback() {
1331
+ val context = requireContext()
1332
+ if (ringbackPlayer?.isPlaying == true) return
1333
+
1334
+ try {
1335
+ val ringbackUri =
1336
+ Uri.parse("android.resource://${context.packageName}/raw/ringback_tone")
1337
+ ringbackPlayer = MediaPlayer.create(context, ringbackUri)
1338
+ ringbackPlayer?.apply {
1339
+ isLooping = true
1340
+ start()
1341
+ }
1342
+ } catch (e: Exception) {
1343
+ Log.e(TAG, "Failed to play ringback tone: ${e.message}")
867
1344
  }
868
-
869
- fun onApplicationTerminate() {
870
- Log.d(TAG, "Application terminating")
871
- activeCalls.keys.toList().forEach { callId ->
872
- telecomConnections[callId]?.let { conn ->
873
- conn.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
874
- conn.destroy()
875
- }
876
- }
877
- activeCalls.clear()
878
- telecomConnections.clear()
879
- callMetadata.clear()
880
- incomingCallIds.clear()
881
- currentCallId = null
882
- cleanup()
883
- audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
884
- eventHandler = null
885
- cachedEvents.clear()
886
- isInitialized.set(false)
887
- appContext = null
1345
+ }
1346
+
1347
+ private fun stopRingback() {
1348
+ try {
1349
+ ringbackPlayer?.stop()
1350
+ ringbackPlayer?.release()
1351
+ } catch (e: Exception) {
1352
+ Log.e(TAG, "Error stopping ringback: ${e.message}")
1353
+ } finally {
1354
+ ringbackPlayer = null
1355
+ }
1356
+ }
1357
+
1358
+ private fun cleanup() {
1359
+ Log.d(TAG, "Performing cleanup")
1360
+ stopForegroundService()
1361
+ keepScreenAwake(false)
1362
+ resetAudioMode()
1363
+ synchronized(ringtoneStateLock) {
1364
+ isCustomRingtoneActive = false
1365
+ }
1366
+ }
1367
+
1368
+ fun onApplicationTerminate() {
1369
+ Log.d(TAG, "Application terminating")
1370
+ activeCalls.keys.toList().forEach { callId ->
1371
+ telecomConnections[callId]?.let { conn ->
1372
+ conn.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
1373
+ conn.destroy()
1374
+ }
888
1375
  }
1376
+ activeCalls.clear()
1377
+ telecomConnections.clear()
1378
+ callMetadata.clear()
1379
+ incomingCallIds.clear() // NEW: Clear duplicate tracking
1380
+ currentCallId = null
1381
+ cleanup()
1382
+ lockScreenBypassCallbacks.clear()
1383
+ eventHandler = null
1384
+ cachedEvents.clear()
1385
+ isInitialized.set(false)
1386
+ appContext = null
1387
+ }
889
1388
  }