@qusaieilouti99/call-manager 0.1.136 → 0.1.138

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