@qusaieilouti99/call-manager 0.1.142 → 0.1.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  package com.margelo.nitro.qusaieilouti99.callmanager
2
2
 
3
3
  import android.app.ActivityManager
4
+ import android.app.KeyguardManager
4
5
  import android.app.Notification
5
6
  import android.app.NotificationChannel
6
7
  import android.app.NotificationManager
@@ -8,8 +9,6 @@ import android.app.PendingIntent
8
9
  import android.content.ComponentName
9
10
  import android.content.Context
10
11
  import android.content.Intent
11
- import android.graphics.Color
12
- import android.media.AudioAttributes
13
12
  import android.media.AudioDeviceCallback
14
13
  import android.media.AudioDeviceInfo
15
14
  import android.media.AudioManager
@@ -21,6 +20,8 @@ import android.os.Bundle
21
20
  import android.os.Handler
22
21
  import android.os.Looper
23
22
  import android.os.PowerManager
23
+ import android.os.VibrationEffect
24
+ import android.os.Vibrator
24
25
  import android.telecom.Connection
25
26
  import android.telecom.DisconnectCause
26
27
  import android.telecom.PhoneAccount
@@ -32,1357 +33,797 @@ import org.json.JSONObject
32
33
  import java.util.concurrent.ConcurrentHashMap
33
34
  import java.util.concurrent.CopyOnWriteArrayList
34
35
  import java.util.concurrent.atomic.AtomicBoolean
35
- import android.app.KeyguardManager
36
- import android.os.Vibrator
37
- import android.os.VibrationEffect
38
36
 
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
- */
46
37
  object CallEngine {
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
- }
110
- }
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
- }
135
- }
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))
146
- }
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
- }
205
- }
206
- }
207
-
208
- fun isLockScreenBypassActive(): Boolean = lockScreenBypassActive
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
209
42
 
210
- fun addTelecomConnection(callId: String, connection: Connection) {
211
- telecomConnections[callId] = connection
212
- Log.d(TAG, "Added Telecom Connection for callId: $callId")
213
- }
214
-
215
- fun removeTelecomConnection(callId: String) {
216
- telecomConnections.remove(callId)
217
- Log.d(TAG, "Removed Telecom Connection for callId: $callId")
218
- }
219
-
220
- fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
43
+ interface CallEndListener {
44
+ fun onCallEnded(callId: String)
45
+ }
221
46
 
222
- fun setCanMakeMultipleCalls(allow: Boolean) {
223
- canMakeMultipleCalls = allow
224
- Log.d(TAG, "canMakeMultipleCalls set to: $allow")
225
- }
47
+ private val callEndListeners = CopyOnWriteArrayList<CallEndListener>()
48
+ private val mainHandler = Handler(Looper.getMainLooper())
226
49
 
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)
50
+ fun registerCallEndListener(l: CallEndListener) {
51
+ callEndListeners.add(l)
246
52
  }
247
53
 
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
54
+ fun unregisterCallEndListener(l: CallEndListener) {
55
+ callEndListeners.remove(l)
254
56
  }
255
- incomingCallIds[callId] = currentTime
256
57
 
257
- // Clean up old entries (older than 30 seconds)
258
- val cutoffTime = currentTime - 30000
259
- incomingCallIds.entries.removeAll { it.value < cutoffTime }
58
+ @Volatile private var appContext: Context? = null
59
+ private val isInitialized = AtomicBoolean(false)
60
+ private val initializationLock = Any()
260
61
 
261
- Log.d(TAG, "reportIncomingCall: callId=$callId, type=$callType, name=$displayName")
262
- metadata?.let { callMetadata[callId] = it }
62
+ private var ringtone: android.media.Ringtone? = null
63
+ private var ringbackPlayer: MediaPlayer? = null
64
+ private var vibrator: Vibrator? = null
65
+ private var audioManager: AudioManager? = null
66
+ private var wakeLock: PowerManager.WakeLock? = null
263
67
 
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
269
- }
68
+ private val activeCalls = ConcurrentHashMap<String, CallInfo>()
69
+ private val telecomConnections = ConcurrentHashMap<String, Connection>()
70
+ private val callMetadata = ConcurrentHashMap<String, String>()
71
+ private val incomingCallIds = ConcurrentHashMap<String, Long>()
270
72
 
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
278
- }
73
+ private var currentCallId: String? = null
74
+ private var canMakeMultipleCalls: Boolean = false
75
+ private var eventHandler: ((CallEventType, String) -> Unit)? = null
76
+ private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
77
+
78
+ // --- Modern Audio Management State ---
79
+ private var userSelectedAudioRoute: String? = null
80
+ private val audioStateLock = Any()
279
81
 
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)
82
+ fun initialize(context: Context) {
83
+ synchronized(initializationLock) {
84
+ if (isInitialized.compareAndSet(false, true)) {
85
+ appContext = context.applicationContext
86
+ audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
87
+ registerAudioDeviceCallback() // Register callback on init
88
+ Log.d(TAG, "CallEngine initialized successfully")
89
+ }
285
90
  }
286
- }
287
91
  }
288
92
 
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) }
306
- }
93
+ fun isInitialized(): Boolean = isInitialized.get()
307
94
 
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)
95
+ private fun requireContext(): Context {
96
+ return appContext ?: throw IllegalStateException(
97
+ "CallEngine not initialized. Call initialize() in Application.onCreate()"
98
+ )
315
99
  }
316
100
 
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
337
- }
101
+ fun getContext(): Context? = appContext
338
102
 
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)
103
+ fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
104
+ Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
105
+ eventHandler = handler
106
+ handler?.let { h ->
107
+ if (cachedEvents.isNotEmpty()) {
108
+ Log.d(TAG, "Emitting ${cachedEvents.size} cached events.")
109
+ cachedEvents.forEach { (type, data) -> h.invoke(type, data) }
110
+ cachedEvents.clear()
111
+ }
344
112
  }
345
- }
346
113
  }
347
114
 
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)
115
+ fun emitEvent(type: CallEventType, data: JSONObject) {
116
+ Log.d(TAG, "Emitting event: $type")
117
+ val dataString = data.toString()
118
+ if (eventHandler != null) {
119
+ eventHandler?.invoke(type, dataString)
120
+ } else {
121
+ Log.d(TAG, "No event handler, caching event: $type")
122
+ cachedEvents.add(Pair(type, dataString))
123
+ }
124
+ }
359
125
 
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) }
126
+ private fun supportsCallStyleNotifications(): Boolean {
127
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
128
+ val manufacturer = Build.MANUFACTURER.lowercase()
129
+ val supportedManufacturers = setOf("google", "samsung")
130
+ return supportedManufacturers.contains(manufacturer)
366
131
  }
367
132
 
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)
133
+ fun silenceIncomingCall() {
134
+ Log.d(TAG, "Silencing incoming call ringtone via Connection.onSilence()")
135
+ stopRingtone()
372
136
  }
373
137
 
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)
138
+ fun addTelecomConnection(callId: String, connection: Connection) {
139
+ telecomConnections[callId] = connection
384
140
  }
385
141
 
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
142
+ fun removeTelecomConnection(callId: String) {
143
+ telecomConnections.remove(callId)
401
144
  }
402
145
 
403
- if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
404
- activeCalls.values.forEach {
405
- if (it.state == CallState.ACTIVE) {
406
- holdCallInternal(it.callId, heldBySystem = false)
146
+ fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
147
+
148
+ fun reportIncomingCall(
149
+ context: Context,
150
+ callId: String,
151
+ callType: String,
152
+ displayName: String,
153
+ pictureUrl: String? = null,
154
+ metadata: String? = null
155
+ ) {
156
+ if (!isInitialized.get()) {
157
+ initialize(context)
158
+ }
159
+
160
+ val currentTime = System.currentTimeMillis()
161
+ if (incomingCallIds.containsKey(callId) || activeCalls.containsKey(callId)) {
162
+ Log.w(TAG, "Ignoring duplicate incoming call report for callId: $callId")
163
+ return
164
+ }
165
+ incomingCallIds[callId] = currentTime
166
+
167
+ Log.d(TAG, "reportIncomingCall: callId=$callId, type=$callType, name=$displayName")
168
+ metadata?.let { callMetadata[callId] = it }
169
+
170
+ activeCalls[callId] =
171
+ CallInfo(callId, callType, displayName, pictureUrl, CallState.INCOMING)
172
+ currentCallId = callId
173
+
174
+ showIncomingCallUI(callId, displayName, callType, pictureUrl)
175
+ registerPhoneAccount()
176
+
177
+ val telecomManager =
178
+ requireContext().getSystemService(Context.TELECOM_SERVICE) as TelecomManager
179
+ val phoneAccountHandle = getPhoneAccountHandle()
180
+ val extras = Bundle().apply {
181
+ putString(MyConnectionService.EXTRA_CALL_ID, callId)
182
+ putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
183
+ putString(MyConnectionService.EXTRA_DISPLAY_NAME, displayName)
184
+ putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, callType == "Video")
185
+ pictureUrl?.let { putString(MyConnectionService.EXTRA_PICTURE_URL, it) }
407
186
  }
408
- }
409
- }
410
187
 
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
188
+ try {
189
+ telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
190
+ startForegroundService()
191
+ } catch (e: Exception) {
192
+ Log.e(TAG, "Failed to report incoming call: ${e.message}", e)
193
+ endCallInternal(callId)
194
+ }
447
195
  }
448
196
 
449
- activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
450
- currentCallId = callId
451
- Log.d(TAG, "Call $callId set to ACTIVE state")
197
+ fun startOutgoingCall(
198
+ callId: String,
199
+ callType: String,
200
+ targetName: String,
201
+ metadata: String? = null
202
+ ) {
203
+ val context = requireContext()
204
+ Log.d(TAG, "startOutgoingCall: callId=$callId, type=$callType, target=$targetName")
205
+ metadata?.let { callMetadata[callId] = it }
206
+
207
+ activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.DIALING)
208
+ currentCallId = callId
209
+ Log.d(TAG, "Call $callId added to activeCalls. State: DIALING")
210
+
211
+ registerPhoneAccount()
212
+
213
+ val telecomManager =
214
+ context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
215
+ val phoneAccountHandle = getPhoneAccountHandle()
216
+ val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
217
+
218
+ val outgoingExtras = Bundle().apply {
219
+ putString(MyConnectionService.EXTRA_CALL_ID, callId)
220
+ putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
221
+ putString(MyConnectionService.EXTRA_DISPLAY_NAME, targetName)
222
+ putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, callType == "Video")
223
+ metadata?.let { putString("metadata", it) }
224
+ }
452
225
 
453
- stopRingtone()
454
- stopRingback()
455
- cancelIncomingCallUI()
226
+ val extras = Bundle().apply {
227
+ putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
228
+ putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
229
+ putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, callType == "Video")
230
+ }
456
231
 
457
- if (!canMakeMultipleCalls) {
458
- activeCalls.filter { it.key != callId }.values.forEach { otherCall ->
459
- if (otherCall.state == CallState.ACTIVE) {
460
- holdCallInternal(otherCall.callId, heldBySystem = false)
232
+ try {
233
+ telecomManager.placeCall(addressUri, extras)
234
+ startForegroundService()
235
+ startRingback()
236
+ bringAppToForeground()
237
+ keepScreenAwake(true)
238
+ Log.d(TAG, "Successfully reported outgoing call to TelecomManager")
239
+ } catch (e: Exception) {
240
+ Log.e(TAG, "Failed to start outgoing call: ${e.message}", e)
241
+ endCallInternal(callId)
461
242
  }
462
- }
463
243
  }
464
244
 
465
- bringAppToForeground()
466
- startForegroundService()
467
- keepScreenAwake(true)
468
- updateLockScreenBypass()
245
+ fun startCall(
246
+ callId: String,
247
+ callType: String,
248
+ targetName: String,
249
+ metadata: String? = null
250
+ ) {
251
+ Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
252
+ metadata?.let { callMetadata[callId] = it }
253
+
254
+ if (activeCalls.containsKey(callId)) {
255
+ Log.w(TAG, "Call $callId already exists, cannot start again")
256
+ return
257
+ }
469
258
 
470
- setAudioMode()
259
+ activeCalls[callId] =
260
+ CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
261
+ currentCallId = callId
262
+ Log.d(TAG, "Call $callId started as ACTIVE")
471
263
 
472
- // NEW: Improved initial audio route with longer delay and retry mechanism
473
- mainHandler.postDelayed({
474
- setInitialAudioRoute(callInfo.callType, isCallStart = true)
264
+ registerPhoneAccount()
265
+ bringAppToForeground()
266
+ startForegroundService()
267
+ keepScreenAwake(true)
475
268
 
476
- // Retry after additional delay if needed for video calls
477
- if (callInfo.callType == "Video") {
269
+ synchronized(audioStateLock) {
270
+ userSelectedAudioRoute = null
271
+ }
478
272
  mainHandler.postDelayed({
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)
273
+ updateAndApplyAudioRoute()
274
+ }, 300)
275
+
276
+ emitEvent(CallEventType.OUTGOING_CALL_ANSWERED, JSONObject().apply {
277
+ put("callId", callId)
278
+ put("callType", callType)
279
+ put("displayName", targetName)
280
+ })
492
281
  }
493
282
 
494
- Log.d(TAG, "Call $callId successfully answered")
495
- }
283
+ fun answerCall(callId: String) {
284
+ Log.d(TAG, "answerCall: $callId - local party answering")
285
+ coreCallAnswered(callId, isLocalAnswer = true)
286
+ }
496
287
 
497
- private fun emitCallAnsweredWithMetadata(callId: String) {
498
- val callInfo = activeCalls[callId] ?: return
499
- val metadata = callMetadata[callId]
288
+ private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
289
+ val callInfo = activeCalls[callId] ?: return
290
+ activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
291
+ currentCallId = callId
500
292
 
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)
293
+ stopRingtone()
294
+ stopRingback()
295
+ cancelIncomingCallUI()
296
+
297
+ bringAppToForeground()
298
+ startForegroundService()
299
+ keepScreenAwake(true)
300
+
301
+ // Set initial audio route using the new robust system
302
+ synchronized(audioStateLock) {
303
+ userSelectedAudioRoute = null // Reset any previous user selection
511
304
  }
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)
305
+ // Give the audio system a moment to initialize after the call becomes active
306
+ mainHandler.postDelayed({
307
+ updateAndApplyAudioRoute()
308
+ }, 300)
309
+
310
+ if (isLocalAnswer) {
311
+ emitCallAnsweredWithMetadata(callId)
530
312
  }
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
545
313
  }
546
314
 
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)
551
- }
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
315
+ private fun emitCallAnsweredWithMetadata(callId: String) {
316
+ val callInfo = activeCalls[callId] ?: return
317
+ emitEvent(CallEventType.CALL_ANSWERED, JSONObject().apply {
318
+ put("callId", callId)
319
+ put("callType", callInfo.callType)
320
+ put("displayName", callInfo.displayName)
321
+ callInfo.pictureUrl?.let { put("pictureUrl", it) }
322
+ })
560
323
  }
561
324
 
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
325
+ fun holdCall(callId: String) {
326
+ val callInfo = activeCalls[callId]
327
+ if (callInfo?.state != CallState.ACTIVE) {
328
+ Log.w(TAG, "Cannot hold call $callId - not in active state")
329
+ return
330
+ }
331
+ activeCalls[callId] = callInfo.copy(state = CallState.HELD)
332
+ telecomConnections[callId]?.setOnHold()
333
+ updateForegroundNotification()
334
+ emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
583
335
  }
584
336
 
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
337
+ fun unholdCall(callId: String) {
338
+ val callInfo = activeCalls[callId]
339
+ if (callInfo?.state != CallState.HELD) {
340
+ Log.w(TAG, "Cannot unhold call $callId - not in held state")
341
+ return
342
+ }
343
+ activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
344
+ telecomConnections[callId]?.setActive()
345
+ updateForegroundNotification()
346
+ emitEvent(CallEventType.CALL_UNHELD, JSONObject().put("callId", callId))
613
347
  }
614
348
 
615
- val context = requireContext()
616
- audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
617
-
618
- val wasMuted = audioManager?.isMicrophoneMute ?: false
619
- audioManager?.isMicrophoneMute = muted
620
-
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")
349
+ fun setMuted(callId: String, muted: Boolean) {
350
+ val callInfo = activeCalls[callId]
351
+ if (callInfo == null) {
352
+ Log.w(TAG, "Cannot set mute state for call $callId - not found")
353
+ return
354
+ }
355
+ val am = audioManager ?: return
356
+ am.isMicrophoneMute = muted
357
+ val eventType = if (muted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
358
+ emitEvent(eventType, JSONObject().put("callId", callId))
625
359
  }
626
- }
627
360
 
628
- fun endCall(callId: String) {
629
- Log.d(TAG, "endCall: $callId")
630
- endCallInternal(callId)
631
- }
361
+ fun endCall(callId: String) {
362
+ endCallInternal(callId)
363
+ }
632
364
 
633
- fun endAllCalls() {
634
- Log.d(TAG, "endAllCalls: Ending all active calls")
635
- if (activeCalls.isEmpty()) return
365
+ fun endAllCalls() {
366
+ Log.d(TAG, "endAllCalls: Ending all active calls")
367
+ if (activeCalls.isEmpty()) return
636
368
 
637
- activeCalls.keys.toList().forEach { callId ->
638
- endCallInternal(callId)
369
+ activeCalls.keys.toList().forEach { callId ->
370
+ endCallInternal(callId)
371
+ }
639
372
  }
640
373
 
641
- activeCalls.clear()
642
- telecomConnections.clear()
643
- callMetadata.clear()
644
- incomingCallIds.clear() // NEW: Clear duplicate tracking
645
- currentCallId = null
374
+ private fun endCallInternal(callId: String) {
375
+ val callInfo = activeCalls.remove(callId) ?: return
376
+ incomingCallIds.remove(callId)
377
+ callMetadata.remove(callId)
646
378
 
647
- cleanup()
648
- updateLockScreenBypass()
649
- }
379
+ stopRingtone()
380
+ stopRingback()
381
+ cancelIncomingCallUI()
650
382
 
651
- private fun endCallInternal(callId: String) {
652
- Log.d(TAG, "endCallInternal: $callId")
383
+ telecomConnections.remove(callId)?.let {
384
+ it.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
385
+ it.destroy()
386
+ }
653
387
 
654
- val callInfo = activeCalls[callId] ?: run {
655
- Log.w(TAG, "Call $callId not found in active calls")
656
- return
388
+ if (activeCalls.isEmpty()) {
389
+ cleanup()
390
+ } else {
391
+ updateForegroundNotification()
392
+ }
393
+
394
+ callEndListeners.forEach { it.onCallEnded(callId) }
395
+ emitEvent(CallEventType.CALL_ENDED, JSONObject().put("callId", callId))
657
396
  }
658
397
 
659
- val metadata = callMetadata.remove(callId)
660
- activeCalls.remove(callId)
661
- incomingCallIds.remove(callId) // NEW: Clean up duplicate tracking
398
+ /**
399
+ * Sets the user's desired audio route. This will be respected until a
400
+ * higher-priority device connects or this route becomes unavailable.
401
+ */
402
+ fun setAudioRoute(route: String) {
403
+ synchronized(audioStateLock) {
404
+ Log.d(TAG, "User requested audio route: $route")
405
+ userSelectedAudioRoute = route
406
+ }
407
+ updateAndApplyAudioRoute()
408
+ }
409
+
410
+ /**
411
+ * Central function to determine and apply the correct audio route.
412
+ * This is the single source of truth for audio routing decisions.
413
+ */
414
+ private fun updateAndApplyAudioRoute() {
415
+ val call = activeCalls[currentCallId]
416
+ if (call == null || call.state != CallState.ACTIVE) {
417
+ Log.d(TAG, "Skipping audio route update: No active call.")
418
+ return
419
+ }
662
420
 
663
- stopRingback()
664
- stopRingtone()
665
- cancelIncomingCallUI()
421
+ synchronized(audioStateLock) {
422
+ val am = audioManager ?: return
423
+ val availableDevices = getAvailableAudioDevices()
424
+
425
+ // Determine the target route based on priority
426
+ val targetRoute = if (userSelectedAudioRoute != null && availableDevices.contains(userSelectedAudioRoute)) {
427
+ userSelectedAudioRoute!! // Respect user's choice if available
428
+ } else {
429
+ // Auto-select based on priority
430
+ when {
431
+ availableDevices.contains("Bluetooth") -> "Bluetooth"
432
+ availableDevices.contains("Headset") -> "Headset"
433
+ call.callType == "Video" -> "Speaker"
434
+ else -> "Earpiece"
435
+ }
436
+ }
666
437
 
667
- if (currentCallId == callId) {
668
- currentCallId =
669
- activeCalls.filter { it.value.state != CallState.ENDED }.keys.firstOrNull()
438
+ Log.d(TAG, "Updating audio route. Available: $availableDevices, User Pref: $userSelectedAudioRoute, Target: $targetRoute")
439
+
440
+ // Apply the target route
441
+ am.mode = AudioManager.MODE_IN_COMMUNICATION
442
+
443
+ when (targetRoute) {
444
+ "Bluetooth" -> {
445
+ am.isSpeakerphoneOn = false
446
+ am.startBluetoothSco()
447
+ am.isBluetoothScoOn = true
448
+ }
449
+ "Headset" -> {
450
+ am.isSpeakerphoneOn = false
451
+ am.stopBluetoothSco()
452
+ am.isBluetoothScoOn = false
453
+ }
454
+ "Speaker" -> {
455
+ am.isSpeakerphoneOn = true
456
+ am.stopBluetoothSco()
457
+ am.isBluetoothScoOn = false
458
+ }
459
+ "Earpiece" -> {
460
+ am.isSpeakerphoneOn = false
461
+ am.stopBluetoothSco()
462
+ am.isBluetoothScoOn = false
463
+ }
464
+ }
465
+
466
+ // After applying, emit the result
467
+ mainHandler.postDelayed({ emitAudioRouteChanged() }, 100)
468
+ }
670
469
  }
671
470
 
672
- val context = requireContext()
673
- val closeActivityIntent = Intent("com.qusaieilouti99.callmanager.CLOSE_CALL_ACTIVITY")
674
- .setPackage(context.packageName)
675
- .putExtra("callId", callId)
471
+ private fun getAvailableAudioDevices(): Set<String> {
472
+ val am = audioManager ?: return emptySet()
473
+ val devices = mutableSetOf<String>()
474
+ devices.add("Earpiece") // Always available
475
+ devices.add("Speaker") // Always available
676
476
 
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}")
477
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
478
+ val audioDevices = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
479
+ audioDevices.forEach {
480
+ when (it.type) {
481
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> devices.add("Bluetooth")
482
+ AudioDeviceInfo.TYPE_WIRED_HEADSET, AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> devices.add("Headset")
483
+ }
484
+ }
485
+ } else {
486
+ @Suppress("DEPRECATION")
487
+ if (am.isBluetoothScoAvailableOffCall) devices.add("Bluetooth")
488
+ @Suppress("DEPRECATION")
489
+ if (am.isWiredHeadsetOn) devices.add("Headset")
490
+ }
491
+ return devices
682
492
  }
683
493
 
684
- telecomConnections[callId]?.let { connection ->
685
- connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
686
- connection.destroy()
687
- removeTelecomConnection(callId)
494
+ fun getAudioDevices(): AudioRoutesInfo {
495
+ val available = getAvailableAudioDevices().map { StringHolder(it) }.toTypedArray()
496
+ val current = getCurrentAudioRoute()
497
+ return AudioRoutesInfo(available, current)
688
498
  }
689
499
 
690
- if (activeCalls.isEmpty()) {
691
- cleanup()
692
- } else {
693
- updateForegroundNotification()
500
+ private fun getCurrentAudioRoute(): String {
501
+ val am = audioManager ?: return "Unknown"
502
+ return when {
503
+ am.isBluetoothScoOn -> "Bluetooth"
504
+ am.isWiredHeadsetOn -> "Headset"
505
+ am.isSpeakerphoneOn -> "Speaker"
506
+ else -> "Earpiece"
507
+ }
694
508
  }
695
509
 
696
- updateLockScreenBypass()
697
-
698
- for (listener in callEndListeners) {
699
- mainHandler.post {
700
- try {
701
- listener.onCallEnded(callId)
702
- } catch (_: Throwable) {
703
- // swallow
510
+ private fun emitAudioRouteChanged() {
511
+ val info = getAudioDevices()
512
+ val payload = JSONObject().apply {
513
+ put("devices", JSONArray(info.devices.map { it.value }))
514
+ put("currentRoute", info.currentRoute)
704
515
  }
705
- }
516
+ emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, payload)
517
+ Log.d(TAG, "Emitted AUDIO_ROUTE_CHANGED: Current='${info.currentRoute}', Available=${info.devices.map { it.value }}")
706
518
  }
707
519
 
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")
520
+ private val audioDeviceCallback = object : AudioDeviceCallback() {
521
+ override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
522
+ Log.d(TAG, "Audio devices added. Triggering audio route update.")
523
+ // A new device was added, reset user preference to allow auto-switch to higher priority device
524
+ synchronized(audioStateLock) {
525
+ userSelectedAudioRoute = null
526
+ }
527
+ updateAndApplyAudioRoute()
528
+ }
529
+ override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
530
+ Log.d(TAG, "Audio devices removed. Triggering audio route update.")
531
+ // A device was removed, if it was the user's selection, clear it
532
+ synchronized(audioStateLock) {
533
+ val removedDeviceTypes = removedDevices.map {
534
+ when(it.type) {
535
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "Bluetooth"
536
+ AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Headset"
537
+ else -> ""
538
+ }
539
+ }
540
+ if (removedDeviceTypes.contains(userSelectedAudioRoute)) {
541
+ userSelectedAudioRoute = null
542
+ }
543
+ }
544
+ updateAndApplyAudioRoute()
734
545
  }
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
546
  }
743
547
 
744
- val current = getCurrentAudioRoute()
745
- Log.d(TAG, "Available audio devices: ${devices.toList()}, current: $current")
746
-
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
- }
752
-
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
548
+ private fun registerAudioDeviceCallback() {
549
+ audioManager?.registerAudioDeviceCallback(audioDeviceCallback, mainHandler)
759
550
  }
760
- val am = audioManager!!
761
551
 
762
- if (am.mode != AudioManager.MODE_IN_COMMUNICATION) {
763
- am.mode = AudioManager.MODE_IN_COMMUNICATION
552
+ private fun cleanup() {
553
+ Log.d(TAG, "Performing cleanup")
554
+ stopForegroundService()
555
+ keepScreenAwake(false)
556
+ // Reset audio state
557
+ synchronized(audioStateLock) {
558
+ userSelectedAudioRoute = null
559
+ }
560
+ audioManager?.mode = AudioManager.MODE_NORMAL
561
+ audioManager?.isSpeakerphoneOn = false
562
+ audioManager?.stopBluetoothSco()
764
563
  }
765
564
 
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
785
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
786
- am.startBluetoothSco()
787
- am.isBluetoothScoOn = true
788
- Log.d(TAG, "Audio routed to BLUETOOTH")
789
- } else {
790
- Log.w(TAG, "Bluetooth SCO not supported on this OS version")
791
- }
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
565
+ private fun createNotificationChannel() {
566
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
567
+ val context = requireContext()
568
+ val channel = NotificationChannel(
569
+ NOTIF_CHANNEL_ID,
570
+ "Incoming Call Channel",
571
+ NotificationManager.IMPORTANCE_HIGH
572
+ ).apply {
573
+ description = "Notifications for incoming calls"
574
+ setBypassDnd(true)
575
+ lockscreenVisibility = Notification.VISIBILITY_PUBLIC
576
+ // **CRITICAL FIX**: Set sound to null. We will manage the ringtone manually.
577
+ setSound(null, null)
578
+ }
579
+ val manager = context.getSystemService(NotificationManager::class.java)
580
+ manager.createNotificationChannel(channel)
798
581
  }
799
- Log.d(TAG, "Audio routed to HEADSET")
800
- }
801
- else -> {
802
- Log.w(TAG, "Unknown audio route: $route")
803
- return
804
- }
805
- }
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"
815
- }
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"
829
582
  }
830
583
 
831
- Log.d(TAG, "Setting initial audio route: $defaultRoute for call type: $callType (isCallStart: $isCallStart)")
584
+ private fun showIncomingCallUI(callId: String, callerName: String, callType: String, callerPicUrl: String?) {
585
+ val context = requireContext()
586
+ Log.d(TAG, "Showing incoming call UI for $callId")
587
+ createNotificationChannel() // Ensure channel is created with correct settings
832
588
 
833
- // For call start, ensure audio mode is properly set first
834
- if (isCallStart) {
835
- setAudioMode()
589
+ showCallActivityOverlay(context, callId, callerName, callType, callerPicUrl)
590
+ showStandardNotification(context, callId, callerName, callType, callerPicUrl)
836
591
 
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")
848
- }
849
- }, 300L)
850
- }, 200L)
851
- } else {
852
- setAudioRoute(defaultRoute)
853
- }
854
- } else {
855
- setAudioRoute(defaultRoute)
856
- }
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")
871
- }
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)
881
- }
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()
592
+ playRingtone()
890
593
  }
891
- override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
892
- Log.d(TAG, "Audio devices removed")
893
- emitAudioDevicesChanged()
894
- }
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)
904
- }
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"
929
- )
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")
594
+
595
+ private fun showCallActivityOverlay(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
596
+ val overlayIntent = Intent(context, CallActivity::class.java).apply {
597
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
598
+ putExtra("callId", callId)
599
+ putExtra("callerName", callerName)
600
+ putExtra("callType", callType)
601
+ callerPicUrl?.let { putExtra("callerAvatar", it) }
602
+ }
603
+ try {
604
+ context.startActivity(overlayIntent)
605
+ } catch (e: Exception) {
606
+ Log.e(TAG, "Failed to launch CallActivity overlay: ${e.message}")
938
607
  }
939
- }
940
- wakeLock = null
941
- }
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
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
608
  }
1000
- }
1001
609
 
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")
610
+ private fun showStandardNotification(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
611
+ val fullScreenIntent = Intent(context, CallActivity::class.java).apply {
612
+ putExtra("callId", callId)
613
+ }
614
+ val fullScreenPendingIntent = PendingIntent.getActivity(
615
+ context, callId.hashCode(), fullScreenIntent,
616
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
617
+ )
1005
618
 
1006
- val useCallStyleNotification = supportsCallStyleNotifications()
1007
- Log.d(TAG, "Using CallStyle notification: $useCallStyleNotification")
619
+ val builder = Notification.Builder(context, NOTIF_CHANNEL_ID)
620
+ .setSmallIcon(android.R.drawable.sym_call_incoming)
621
+ .setContentTitle("Incoming Call")
622
+ .setContentText(callerName)
623
+ .setPriority(Notification.PRIORITY_MAX)
624
+ .setCategory(Notification.CATEGORY_CALL)
625
+ .setFullScreenIntent(fullScreenPendingIntent, true)
626
+ .setOngoing(true)
627
+
628
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && supportsCallStyleNotifications()) {
629
+ val person = android.app.Person.Builder().setName(callerName).build()
630
+ val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply { action = "com.qusaieilouti99.callmanager.ANSWER_CALL"; putExtra("callId", callId) }
631
+ val declineIntent = Intent(context, CallNotificationActionReceiver::class.java).apply { action = "com.qusaieilouti99.callmanager.DECLINE_CALL"; putExtra("callId", callId) }
632
+ val answerPI = PendingIntent.getBroadcast(context, callId.hashCode() + 1, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
633
+ val declinePI = PendingIntent.getBroadcast(context, callId.hashCode() + 2, declineIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
634
+ builder.setStyle(Notification.CallStyle.forIncomingCall(person, declinePI, answerPI))
635
+ }
1008
636
 
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)
637
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
638
+ notificationManager.notify(NOTIF_ID, builder.build())
1015
639
  }
1016
640
 
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)
641
+ fun cancelIncomingCallUI() {
642
+ val context = requireContext()
643
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
644
+ notificationManager.cancel(NOTIF_ID)
645
+ stopRingtone()
1039
646
  }
1040
647
 
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
- }
648
+ private fun playRingtone() {
649
+ if (ringtone?.isPlaying == true) return
650
+ val context = requireContext()
651
+ audioManager?.mode = AudioManager.MODE_RINGTONE
1067
652
 
1068
- val fullScreenPendingIntent = PendingIntent.getActivity(
1069
- context, callId.hashCode(), fullScreenIntent,
1070
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
1071
- )
653
+ vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
654
+ val pattern = longArrayOf(0, 1000, 500)
655
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
656
+ vibrator?.vibrate(VibrationEffect.createWaveform(pattern, 0))
657
+ } else {
658
+ @Suppress("DEPRECATION")
659
+ vibrator?.vibrate(pattern, 0)
660
+ }
1072
661
 
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()
662
+ try {
663
+ val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
664
+ ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
665
+ ringtone?.play()
666
+ Log.d(TAG, "Manual ringtone started.")
667
+ } catch (e: Exception) {
668
+ Log.e(TAG, "Failed to play ringtone", e)
669
+ }
1126
670
  }
1127
671
 
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
672
+ fun stopRingtone() {
673
+ vibrator?.cancel()
674
+ vibrator = null
675
+ ringtone?.stop()
676
+ ringtone = null
677
+ Log.d(TAG, "Manual ringtone stopped.")
1146
678
  }
1147
679
 
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)
1154
- }
680
+ private fun startRingback() {
681
+ val context = requireContext()
682
+ if (ringbackPlayer?.isPlaying == true) return
1155
683
 
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
684
+ try {
685
+ val ringbackUri =
686
+ Uri.parse("android.resource://${context.packageName}/raw/ringback_tone")
687
+ ringbackPlayer = MediaPlayer.create(context, ringbackUri)
688
+ ringbackPlayer?.apply {
689
+ isLooping = true
690
+ start()
691
+ }
692
+ } catch (e: Exception) {
693
+ Log.e(TAG, "Failed to play ringback tone: ${e.message}")
1184
694
  }
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
695
+ }
696
+
697
+ private fun stopRingback() {
698
+ try {
699
+ ringbackPlayer?.stop()
700
+ ringbackPlayer?.release()
701
+ } catch (e: Exception) {
702
+ Log.e(TAG, "Error stopping ringback: ${e.message}")
703
+ } finally {
704
+ ringbackPlayer = null
1195
705
  }
1196
- } catch (e: Exception) {
1197
- Log.w(TAG, "Failed to get running tasks: ${e.message}")
1198
- }
1199
706
  }
1200
- return false
1201
- }
1202
707
 
1203
- private fun bringAppToForeground() {
1204
- if (isMainActivityInForeground()) {
1205
- Log.d(TAG, "MainActivity is already in foreground, skipping")
1206
- return
708
+ fun keepScreenAwake(keepAwake: Boolean) {
709
+ val context = requireContext()
710
+ val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
711
+ if (keepAwake) {
712
+ if (wakeLock == null || wakeLock!!.isHeld.not()) {
713
+ wakeLock = powerManager.newWakeLock(
714
+ PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
715
+ "CallEngine:WakeLock"
716
+ )
717
+ wakeLock?.acquire(10 * 60 * 1000L)
718
+ Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK")
719
+ }
720
+ } else {
721
+ wakeLock?.let {
722
+ if (it.isHeld) {
723
+ it.release()
724
+ Log.d(TAG, "Released SCREEN_DIM_WAKE_LOCK")
725
+ }
726
+ }
727
+ wakeLock = null
728
+ }
1207
729
  }
1208
730
 
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)
731
+ private fun bringAppToForeground() {
732
+ val context = requireContext()
733
+ val packageName = context.packageName
734
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
735
+ launchIntent?.addFlags(
736
+ Intent.FLAG_ACTIVITY_NEW_TASK or
737
+ Intent.FLAG_ACTIVITY_CLEAR_TOP or
738
+ Intent.FLAG_ACTIVITY_SINGLE_TOP
739
+ )
740
+ try {
741
+ context.startActivity(launchIntent)
742
+ } catch (e: Exception) {
743
+ Log.e(TAG, "Failed to bring app to foreground: ${e.message}")
744
+ }
1221
745
  }
1222
746
 
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}")
747
+ private fun registerPhoneAccount() {
748
+ val context = requireContext()
749
+ val telecomManager =
750
+ context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
751
+ val phoneAccountHandle = getPhoneAccountHandle()
752
+
753
+ if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
754
+ val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
755
+ .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
756
+ .build()
757
+
758
+ try {
759
+ telecomManager.registerPhoneAccount(phoneAccount)
760
+ Log.d(TAG, "PhoneAccount registered successfully")
761
+ } catch (e: Exception) {
762
+ Log.e(TAG, "Failed to register PhoneAccount: ${e.message}", e)
763
+ }
764
+ }
1230
765
  }
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
- }
766
+
767
+ private fun getPhoneAccountHandle(): PhoneAccountHandle {
768
+ val context = requireContext()
769
+ return PhoneAccountHandle(
770
+ ComponentName(context, MyConnectionService::class.java),
771
+ PHONE_ACCOUNT_ID
772
+ )
1250
773
  }
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
- }
1277
774
 
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)
775
+ private fun startForegroundService() {
776
+ val context = requireContext()
777
+ val currentCall = activeCalls.values.find {
778
+ it.state == CallState.ACTIVE ||
779
+ it.state == CallState.INCOMING ||
780
+ it.state == CallState.DIALING ||
781
+ it.state == CallState.HELD
1286
782
  }
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
783
+
784
+ val intent = Intent(context, CallForegroundService::class.java)
785
+ currentCall?.let {
786
+ intent.putExtra("callId", it.callId)
787
+ intent.putExtra("callType", it.callType)
788
+ intent.putExtra("displayName", it.displayName)
789
+ intent.putExtra("state", it.state.name)
1297
790
  }
1298
791
 
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")
792
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
793
+ context.startForegroundService(intent)
1305
794
  } else {
1306
- Log.d(TAG, "Skipping custom ringtone - system should handle it")
795
+ context.startService(intent)
1307
796
  }
1308
- } catch (e: Exception) {
1309
- Log.e(TAG, "Failed to play ringtone", e)
1310
- }
1311
797
  }
1312
- }
1313
798
 
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
1327
- }
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}")
1344
- }
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
799
+ private fun stopForegroundService() {
800
+ val context = requireContext()
801
+ val intent = Intent(context, CallForegroundService::class.java)
802
+ context.stopService(intent)
1355
803
  }
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
804
+
805
+ private fun updateForegroundNotification() {
806
+ startForegroundService()
1365
807
  }
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
- }
808
+
809
+ fun onApplicationTerminate() {
810
+ Log.d(TAG, "Application terminating")
811
+ activeCalls.keys.toList().forEach { callId ->
812
+ telecomConnections[callId]?.let { conn ->
813
+ conn.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
814
+ conn.destroy()
815
+ }
816
+ }
817
+ activeCalls.clear()
818
+ telecomConnections.clear()
819
+ callMetadata.clear()
820
+ incomingCallIds.clear()
821
+ currentCallId = null
822
+ cleanup()
823
+ audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
824
+ eventHandler = null
825
+ cachedEvents.clear()
826
+ isInitialized.set(false)
827
+ appContext = null
1375
828
  }
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
- }
1388
829
  }