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