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