@qusaieilouti99/call-manager 0.1.164 → 0.1.166
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 +352 -303
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/MyConnection.kt +64 -68
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/MyConnectionService.kt +36 -32
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
package com.margelo.nitro.qusaieilouti99.callmanager
|
|
2
2
|
|
|
3
|
-
import android.telecom.CallAudioState
|
|
4
3
|
import android.app.ActivityManager
|
|
4
|
+
import android.app.KeyguardManager
|
|
5
5
|
import android.app.Notification
|
|
6
6
|
import android.app.NotificationChannel
|
|
7
7
|
import android.app.NotificationManager
|
|
@@ -10,9 +10,6 @@ import android.content.ComponentName
|
|
|
10
10
|
import android.content.Context
|
|
11
11
|
import android.content.Intent
|
|
12
12
|
import android.graphics.Color
|
|
13
|
-
import android.media.AudioAttributes
|
|
14
|
-
import android.media.AudioDeviceCallback
|
|
15
|
-
import android.media.AudioDeviceInfo
|
|
16
13
|
import android.media.AudioManager
|
|
17
14
|
import android.media.MediaPlayer
|
|
18
15
|
import android.media.RingtoneManager
|
|
@@ -22,21 +19,29 @@ import android.os.Bundle
|
|
|
22
19
|
import android.os.Handler
|
|
23
20
|
import android.os.Looper
|
|
24
21
|
import android.os.PowerManager
|
|
22
|
+
import android.os.VibrationEffect
|
|
23
|
+
import android.os.Vibrator
|
|
24
|
+
import android.telecom.CallAudioState
|
|
25
25
|
import android.telecom.Connection
|
|
26
26
|
import android.telecom.DisconnectCause
|
|
27
27
|
import android.telecom.PhoneAccount
|
|
28
28
|
import android.telecom.PhoneAccountHandle
|
|
29
29
|
import android.telecom.TelecomManager
|
|
30
30
|
import android.util.Log
|
|
31
|
-
import org.json.JSONArray
|
|
32
|
-
import org.json.JSONObject
|
|
33
31
|
import java.util.concurrent.ConcurrentHashMap
|
|
34
32
|
import java.util.concurrent.CopyOnWriteArrayList
|
|
35
33
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
36
|
-
import
|
|
37
|
-
import
|
|
38
|
-
import android.os.VibrationEffect
|
|
34
|
+
import org.json.JSONArray
|
|
35
|
+
import org.json.JSONObject
|
|
39
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Core call‐management engine. Manages self-managed telecom calls,
|
|
39
|
+
* audio routing, UI notifications, etc.
|
|
40
|
+
*
|
|
41
|
+
* Audio routing is now fully delegated to the Android Telecom framework,
|
|
42
|
+
* which is the correct approach for self-managed calls. This ensures
|
|
43
|
+
* consistency and proper handling of device changes (BT, headset, etc.).
|
|
44
|
+
*/
|
|
40
45
|
object CallEngine {
|
|
41
46
|
private const val TAG = "CallEngine"
|
|
42
47
|
private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
|
|
@@ -79,8 +84,6 @@ object CallEngine {
|
|
|
79
84
|
private var eventHandler: ((CallEventType, String) -> Unit)? = null
|
|
80
85
|
private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
|
|
81
86
|
|
|
82
|
-
private var currentAudioRoute: String = "Unknown"
|
|
83
|
-
|
|
84
87
|
interface LockScreenBypassCallback {
|
|
85
88
|
fun onLockScreenBypassChanged(shouldBypass: Boolean)
|
|
86
89
|
}
|
|
@@ -135,8 +138,12 @@ object CallEngine {
|
|
|
135
138
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
|
|
136
139
|
val manufacturer = Build.MANUFACTURER.lowercase()
|
|
137
140
|
val brand = Build.BRAND.lowercase()
|
|
138
|
-
val supportedManufacturers = setOf(
|
|
139
|
-
|
|
141
|
+
val supportedManufacturers = setOf(
|
|
142
|
+
"google", "samsung", "oneplus", "motorola", "sony", "lg", "htc"
|
|
143
|
+
)
|
|
144
|
+
val supportedBrands = setOf(
|
|
145
|
+
"google", "samsung", "oneplus", "motorola", "sony", "lg", "htc", "pixel"
|
|
146
|
+
)
|
|
140
147
|
val isSupported = supportedManufacturers.contains(manufacturer) ||
|
|
141
148
|
supportedBrands.contains(brand) ||
|
|
142
149
|
manufacturer.contains("google") ||
|
|
@@ -212,20 +219,26 @@ object CallEngine {
|
|
|
212
219
|
if (!isInitialized.get()) {
|
|
213
220
|
initialize(context)
|
|
214
221
|
}
|
|
222
|
+
|
|
215
223
|
Log.d(TAG, "reportIncomingCall: callId=$callId, type=$callType, name=$displayName")
|
|
216
224
|
metadata?.let { callMetadata[callId] = it }
|
|
225
|
+
|
|
217
226
|
val incomingCall = activeCalls.values.find { it.state == CallState.INCOMING }
|
|
218
227
|
if (incomingCall != null) {
|
|
219
228
|
Log.d(TAG, "Incoming call collision detected. Auto-rejecting new call: $callId")
|
|
220
229
|
rejectIncomingCallCollision(callId, "Another call is already incoming")
|
|
221
230
|
return
|
|
222
231
|
}
|
|
223
|
-
|
|
232
|
+
|
|
233
|
+
val activeCall = activeCalls.values.find {
|
|
234
|
+
it.state == CallState.ACTIVE || it.state == CallState.HELD
|
|
235
|
+
}
|
|
224
236
|
if (activeCall != null && !canMakeMultipleCalls) {
|
|
225
237
|
Log.d(TAG, "Active call exists when receiving incoming call. Auto-rejecting: $callId")
|
|
226
238
|
rejectIncomingCallCollision(callId, "Another call is already active")
|
|
227
239
|
return
|
|
228
240
|
}
|
|
241
|
+
|
|
229
242
|
val isVideoCall = callType == "Video"
|
|
230
243
|
if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
|
|
231
244
|
activeCalls.values.forEach {
|
|
@@ -234,12 +247,17 @@ object CallEngine {
|
|
|
234
247
|
}
|
|
235
248
|
}
|
|
236
249
|
}
|
|
237
|
-
|
|
250
|
+
|
|
251
|
+
activeCalls[callId] =
|
|
252
|
+
CallInfo(callId, callType, displayName, pictureUrl, CallState.INCOMING)
|
|
238
253
|
currentCallId = callId
|
|
239
254
|
Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING")
|
|
255
|
+
|
|
240
256
|
showIncomingCallUI(callId, displayName, callType, pictureUrl)
|
|
241
257
|
registerPhoneAccount()
|
|
242
|
-
|
|
258
|
+
|
|
259
|
+
val telecomManager =
|
|
260
|
+
requireContext().getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
243
261
|
val phoneAccountHandle = getPhoneAccountHandle()
|
|
244
262
|
val extras = Bundle().apply {
|
|
245
263
|
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
@@ -248,6 +266,7 @@ object CallEngine {
|
|
|
248
266
|
putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, isVideoCall)
|
|
249
267
|
pictureUrl?.let { putString(MyConnectionService.EXTRA_PICTURE_URL, it) }
|
|
250
268
|
}
|
|
269
|
+
|
|
251
270
|
try {
|
|
252
271
|
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
|
|
253
272
|
startForegroundService()
|
|
@@ -256,6 +275,7 @@ object CallEngine {
|
|
|
256
275
|
Log.e(TAG, "Failed to report incoming call: ${e.message}", e)
|
|
257
276
|
endCallInternal(callId)
|
|
258
277
|
}
|
|
278
|
+
|
|
259
279
|
updateLockScreenBypass()
|
|
260
280
|
}
|
|
261
281
|
|
|
@@ -268,6 +288,7 @@ object CallEngine {
|
|
|
268
288
|
val context = requireContext()
|
|
269
289
|
Log.d(TAG, "startOutgoingCall: callId=$callId, type=$callType, target=$targetName")
|
|
270
290
|
metadata?.let { callMetadata[callId] = it }
|
|
291
|
+
|
|
271
292
|
if (!validateOutgoingCallRequest()) {
|
|
272
293
|
Log.w(TAG, "Rejecting outgoing call - incoming/active call exists")
|
|
273
294
|
emitEvent(CallEventType.CALL_REJECTED, JSONObject().apply {
|
|
@@ -276,6 +297,7 @@ object CallEngine {
|
|
|
276
297
|
})
|
|
277
298
|
return
|
|
278
299
|
}
|
|
300
|
+
|
|
279
301
|
val isVideoCall = callType == "Video"
|
|
280
302
|
if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
|
|
281
303
|
activeCalls.values.forEach {
|
|
@@ -284,14 +306,18 @@ object CallEngine {
|
|
|
284
306
|
}
|
|
285
307
|
}
|
|
286
308
|
}
|
|
309
|
+
|
|
287
310
|
activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.DIALING)
|
|
288
311
|
currentCallId = callId
|
|
289
312
|
Log.d(TAG, "Call $callId added to activeCalls. State: DIALING")
|
|
290
|
-
|
|
313
|
+
|
|
291
314
|
registerPhoneAccount()
|
|
292
|
-
|
|
315
|
+
|
|
316
|
+
val telecomManager =
|
|
317
|
+
context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
293
318
|
val phoneAccountHandle = getPhoneAccountHandle()
|
|
294
319
|
val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
|
|
320
|
+
|
|
295
321
|
val outgoingExtras = Bundle().apply {
|
|
296
322
|
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
297
323
|
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
@@ -299,11 +325,14 @@ object CallEngine {
|
|
|
299
325
|
putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, isVideoCall)
|
|
300
326
|
metadata?.let { putString("metadata", it) }
|
|
301
327
|
}
|
|
328
|
+
|
|
302
329
|
val extras = Bundle().apply {
|
|
303
330
|
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
|
|
304
331
|
putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
|
|
332
|
+
// Let Telecom decide the initial audio route based on devices and video state
|
|
305
333
|
putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, isVideoCall)
|
|
306
334
|
}
|
|
335
|
+
|
|
307
336
|
try {
|
|
308
337
|
telecomManager.placeCall(addressUri, extras)
|
|
309
338
|
startForegroundService()
|
|
@@ -315,9 +344,15 @@ object CallEngine {
|
|
|
315
344
|
Log.e(TAG, "Failed to start outgoing call: ${e.message}", e)
|
|
316
345
|
endCallInternal(callId)
|
|
317
346
|
}
|
|
347
|
+
|
|
318
348
|
updateLockScreenBypass()
|
|
319
349
|
}
|
|
320
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Starts a call and immediately sets it to active.
|
|
353
|
+
* Use this for joining an already-established call.
|
|
354
|
+
* If a call with the same ID is already incoming, it answers it instead.
|
|
355
|
+
*/
|
|
321
356
|
fun startCall(
|
|
322
357
|
callId: String,
|
|
323
358
|
callType: String,
|
|
@@ -325,57 +360,96 @@ object CallEngine {
|
|
|
325
360
|
metadata: String? = null
|
|
326
361
|
) {
|
|
327
362
|
Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
|
|
363
|
+
|
|
328
364
|
val existingCall = activeCalls[callId]
|
|
329
|
-
if (existingCall != null
|
|
330
|
-
|
|
331
|
-
|
|
365
|
+
if (existingCall != null) {
|
|
366
|
+
if (existingCall.state == CallState.INCOMING) {
|
|
367
|
+
Log.d(TAG, "startCall: Call $callId is incoming, answering it.")
|
|
368
|
+
answerCall(callId)
|
|
369
|
+
} else {
|
|
370
|
+
Log.w(TAG, "startCall: Call $callId already exists with state ${existingCall.state}. Ignoring.")
|
|
371
|
+
}
|
|
332
372
|
return
|
|
333
373
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
374
|
+
|
|
375
|
+
metadata?.let { callMetadata[callId] = it }
|
|
376
|
+
|
|
377
|
+
if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
|
|
378
|
+
activeCalls.values.forEach {
|
|
379
|
+
if (it.state == CallState.ACTIVE) {
|
|
380
|
+
holdCallInternal(it.callId, heldBySystem = false)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// This call will be set to ACTIVE immediately by MyConnectionService
|
|
386
|
+
activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
|
|
387
|
+
currentCallId = callId
|
|
388
|
+
Log.d(TAG, "Call $callId will be started as ACTIVE.")
|
|
389
|
+
|
|
390
|
+
registerPhoneAccount()
|
|
391
|
+
|
|
392
|
+
val context = requireContext()
|
|
393
|
+
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
394
|
+
val phoneAccountHandle = getPhoneAccountHandle()
|
|
395
|
+
val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
|
|
396
|
+
|
|
397
|
+
val outgoingExtras = Bundle().apply {
|
|
398
|
+
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
399
|
+
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
400
|
+
putString(MyConnectionService.EXTRA_DISPLAY_NAME, targetName)
|
|
401
|
+
putBoolean(MyConnectionService.EXTRA_IS_VIDEO_CALL_BOOLEAN, callType == "Video")
|
|
402
|
+
putBoolean(MyConnectionService.EXTRA_START_IMMEDIATELY_ACTIVE, true) // Custom flag
|
|
403
|
+
metadata?.let { putString("metadata", it) }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
val extras = Bundle().apply {
|
|
407
|
+
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
|
|
408
|
+
putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
telecomManager.placeCall(addressUri, extras)
|
|
413
|
+
startForegroundService()
|
|
414
|
+
bringAppToForeground()
|
|
415
|
+
keepScreenAwake(true)
|
|
416
|
+
updateLockScreenBypass()
|
|
417
|
+
// Event is emitted by coreCallAnswered once the connection is active
|
|
418
|
+
Log.d(TAG, "Successfully placed call to be immediately active with TelecomManager.")
|
|
419
|
+
} catch (e: Exception) {
|
|
420
|
+
Log.e(TAG, "Failed to start call: ${e.message}", e)
|
|
421
|
+
endCallInternal(callId)
|
|
337
422
|
}
|
|
338
|
-
Log.d(TAG, "startCall: Starting new outgoing call $callId and connecting immediately.")
|
|
339
|
-
startOutgoingCall(callId, callType, targetName, metadata)
|
|
340
|
-
mainHandler.postDelayed({
|
|
341
|
-
callAnsweredFromJS(callId)
|
|
342
|
-
}, 200)
|
|
343
423
|
}
|
|
344
424
|
|
|
345
425
|
fun callAnsweredFromJS(callId: String) {
|
|
346
426
|
Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
|
|
347
|
-
val callInfo = activeCalls[callId]
|
|
348
|
-
if (callInfo?.state != CallState.DIALING) {
|
|
349
|
-
Log.w(TAG, "Cannot answer outgoing call $callId from JS - not in DIALING state. State is ${callInfo?.state}")
|
|
350
|
-
return
|
|
351
|
-
}
|
|
352
427
|
coreCallAnswered(callId, isLocalAnswer = false)
|
|
353
428
|
}
|
|
354
429
|
|
|
355
430
|
fun answerCall(callId: String) {
|
|
356
431
|
Log.d(TAG, "answerCall: $callId - local party answering")
|
|
357
|
-
val callInfo = activeCalls[callId]
|
|
358
|
-
if (callInfo?.state != CallState.INCOMING) {
|
|
359
|
-
Log.w(TAG, "Cannot answer call $callId - not in INCOMING state. State is ${callInfo?.state}")
|
|
360
|
-
return
|
|
361
|
-
}
|
|
362
432
|
coreCallAnswered(callId, isLocalAnswer = true)
|
|
363
433
|
}
|
|
364
434
|
|
|
365
|
-
|
|
435
|
+
fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
|
|
366
436
|
Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
|
|
367
437
|
val callInfo = activeCalls[callId]
|
|
368
438
|
if (callInfo == null) {
|
|
369
439
|
Log.w(TAG, "Cannot answer call $callId - not found in active calls")
|
|
370
440
|
return
|
|
371
441
|
}
|
|
372
|
-
|
|
442
|
+
|
|
443
|
+
// The connection state change (e.g., onAnswer) is the source of truth.
|
|
444
|
+
// We just update our internal state and UI.
|
|
373
445
|
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
374
446
|
currentCallId = callId
|
|
375
447
|
Log.d(TAG, "Call $callId set to ACTIVE state")
|
|
448
|
+
|
|
376
449
|
stopRingtone()
|
|
377
450
|
stopRingback()
|
|
378
451
|
cancelIncomingCallUI()
|
|
452
|
+
|
|
379
453
|
if (!canMakeMultipleCalls) {
|
|
380
454
|
activeCalls.filter { it.key != callId }.values.forEach { otherCall ->
|
|
381
455
|
if (otherCall.state == CallState.ACTIVE) {
|
|
@@ -383,31 +457,39 @@ object CallEngine {
|
|
|
383
457
|
}
|
|
384
458
|
}
|
|
385
459
|
}
|
|
460
|
+
|
|
386
461
|
bringAppToForeground()
|
|
387
462
|
startForegroundService()
|
|
388
463
|
keepScreenAwake(true)
|
|
389
464
|
updateLockScreenBypass()
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
// The initial audio route
|
|
465
|
+
|
|
466
|
+
// Audio is now managed by the Connection. We don't need to do anything here.
|
|
467
|
+
// The initial audio route is set by MyConnection when it becomes active.
|
|
468
|
+
|
|
393
469
|
if (isLocalAnswer) {
|
|
394
470
|
emitCallAnsweredWithMetadata(callId)
|
|
395
471
|
} else {
|
|
396
472
|
emitOutgoingCallAnsweredWithMetadata(callId)
|
|
397
473
|
}
|
|
474
|
+
|
|
398
475
|
Log.d(TAG, "Call $callId successfully answered")
|
|
399
476
|
}
|
|
400
477
|
|
|
401
478
|
private fun emitCallAnsweredWithMetadata(callId: String) {
|
|
402
479
|
val callInfo = activeCalls[callId] ?: return
|
|
403
480
|
val metadata = callMetadata[callId]
|
|
481
|
+
|
|
404
482
|
emitEvent(CallEventType.CALL_ANSWERED, JSONObject().apply {
|
|
405
483
|
put("callId", callId)
|
|
406
484
|
put("callType", callInfo.callType)
|
|
407
485
|
put("displayName", callInfo.displayName)
|
|
408
486
|
callInfo.pictureUrl?.let { put("pictureUrl", it) }
|
|
409
487
|
metadata?.let {
|
|
410
|
-
try {
|
|
488
|
+
try {
|
|
489
|
+
put("metadata", JSONObject(it))
|
|
490
|
+
} catch (e: Exception) {
|
|
491
|
+
put("metadata", it)
|
|
492
|
+
}
|
|
411
493
|
}
|
|
412
494
|
})
|
|
413
495
|
}
|
|
@@ -415,13 +497,18 @@ object CallEngine {
|
|
|
415
497
|
private fun emitOutgoingCallAnsweredWithMetadata(callId: String) {
|
|
416
498
|
val callInfo = activeCalls[callId] ?: return
|
|
417
499
|
val metadata = callMetadata[callId]
|
|
500
|
+
|
|
418
501
|
emitEvent(CallEventType.OUTGOING_CALL_ANSWERED, JSONObject().apply {
|
|
419
502
|
put("callId", callId)
|
|
420
503
|
put("callType", callInfo.callType)
|
|
421
504
|
put("displayName", callInfo.displayName)
|
|
422
505
|
callInfo.pictureUrl?.let { put("pictureUrl", it) }
|
|
423
506
|
metadata?.let {
|
|
424
|
-
try {
|
|
507
|
+
try {
|
|
508
|
+
put("metadata", JSONObject(it))
|
|
509
|
+
} catch (e: Exception) {
|
|
510
|
+
put("metadata", it)
|
|
511
|
+
}
|
|
425
512
|
}
|
|
426
513
|
})
|
|
427
514
|
}
|
|
@@ -437,6 +524,7 @@ object CallEngine {
|
|
|
437
524
|
Log.w(TAG, "Cannot set hold state for call $callId - not found")
|
|
438
525
|
return
|
|
439
526
|
}
|
|
527
|
+
|
|
440
528
|
if (onHold && callInfo.state == CallState.ACTIVE) {
|
|
441
529
|
holdCallInternal(callId, heldBySystem = false)
|
|
442
530
|
} else if (!onHold && callInfo.state == CallState.HELD) {
|
|
@@ -451,7 +539,12 @@ object CallEngine {
|
|
|
451
539
|
Log.w(TAG, "Cannot hold call $callId - not in active state")
|
|
452
540
|
return
|
|
453
541
|
}
|
|
454
|
-
|
|
542
|
+
|
|
543
|
+
activeCalls[callId] = callInfo.copy(
|
|
544
|
+
state = CallState.HELD,
|
|
545
|
+
wasHeldBySystem = heldBySystem
|
|
546
|
+
)
|
|
547
|
+
|
|
455
548
|
telecomConnections[callId]?.setOnHold()
|
|
456
549
|
updateForegroundNotification()
|
|
457
550
|
emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
|
|
@@ -469,7 +562,12 @@ object CallEngine {
|
|
|
469
562
|
Log.w(TAG, "Cannot unhold call $callId - not in held state")
|
|
470
563
|
return
|
|
471
564
|
}
|
|
472
|
-
|
|
565
|
+
|
|
566
|
+
activeCalls[callId] = callInfo.copy(
|
|
567
|
+
state = CallState.ACTIVE,
|
|
568
|
+
wasHeldBySystem = false
|
|
569
|
+
)
|
|
570
|
+
|
|
473
571
|
telecomConnections[callId]?.setActive()
|
|
474
572
|
updateForegroundNotification()
|
|
475
573
|
emitEvent(CallEventType.CALL_UNHELD, JSONObject().put("callId", callId))
|
|
@@ -477,45 +575,25 @@ object CallEngine {
|
|
|
477
575
|
}
|
|
478
576
|
|
|
479
577
|
fun muteCall(callId: String) {
|
|
480
|
-
|
|
578
|
+
setMutedInternal(callId, true)
|
|
481
579
|
}
|
|
482
580
|
|
|
483
581
|
fun unmuteCall(callId: String) {
|
|
484
|
-
|
|
582
|
+
setMutedInternal(callId, false)
|
|
485
583
|
}
|
|
486
584
|
|
|
487
|
-
/**
|
|
488
|
-
* FIX: This is the correct way to handle mute requests from the app's UI.
|
|
489
|
-
* It uses the AudioManager, which is the source of truth for microphone state.
|
|
490
|
-
*/
|
|
491
585
|
fun setMuted(callId: String, muted: Boolean) {
|
|
492
|
-
|
|
493
|
-
Log.w(TAG, "Cannot set mute state for call $callId - not found")
|
|
494
|
-
return
|
|
495
|
-
}
|
|
496
|
-
audioManager?.isMicrophoneMute = muted
|
|
497
|
-
Log.d(TAG, "Set mute state to $muted for call $callId via AudioManager")
|
|
498
|
-
|
|
499
|
-
// We must also emit the event to keep the JS UI in sync.
|
|
500
|
-
onTelecomMuteStateChanged(callId, muted)
|
|
586
|
+
setMutedInternal(callId, muted)
|
|
501
587
|
}
|
|
502
588
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
fun onTelecomMuteStateChanged(callId: String, isMuted: Boolean) {
|
|
508
|
-
if (!activeCalls.containsKey(callId)) {
|
|
509
|
-
Log.w(TAG, "Received mute state change for unknown call $callId")
|
|
589
|
+
private fun setMutedInternal(callId: String, muted: Boolean) {
|
|
590
|
+
val callInfo = activeCalls[callId]
|
|
591
|
+
if (callInfo == null) {
|
|
592
|
+
Log.w(TAG, "Cannot set mute state for call $callId - not found")
|
|
510
593
|
return
|
|
511
594
|
}
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
audioManager?.isMicrophoneMute = isMuted
|
|
515
|
-
}
|
|
516
|
-
Log.d(TAG, "Telecom reported mute state for $callId is now: $isMuted. Emitting event.")
|
|
517
|
-
val eventType = if (isMuted) CallEventType.CALL_MUTED else CallEventType.CALL_UNMUTED
|
|
518
|
-
emitEvent(eventType, JSONObject().put("callId", callId))
|
|
595
|
+
// The Connection's onCallAudioStateChanged will handle the event emission
|
|
596
|
+
(telecomConnections[callId] as? MyConnection)?.setMuted(muted)
|
|
519
597
|
}
|
|
520
598
|
|
|
521
599
|
fun endCall(callId: String) {
|
|
@@ -526,237 +604,169 @@ object CallEngine {
|
|
|
526
604
|
fun endAllCalls() {
|
|
527
605
|
Log.d(TAG, "endAllCalls: Ending all active calls")
|
|
528
606
|
if (activeCalls.isEmpty()) return
|
|
607
|
+
|
|
529
608
|
activeCalls.keys.toList().forEach { callId ->
|
|
530
609
|
endCallInternal(callId)
|
|
531
610
|
}
|
|
532
|
-
activeCalls.clear()
|
|
533
|
-
telecomConnections.clear()
|
|
534
|
-
callMetadata.clear()
|
|
535
|
-
currentCallId = null
|
|
536
|
-
cleanup()
|
|
537
|
-
updateLockScreenBypass()
|
|
538
611
|
}
|
|
539
612
|
|
|
540
613
|
private fun endCallInternal(callId: String) {
|
|
541
614
|
Log.d(TAG, "endCallInternal: $callId")
|
|
615
|
+
|
|
542
616
|
val callInfo = activeCalls[callId] ?: run {
|
|
543
617
|
Log.w(TAG, "Call $callId not found in active calls")
|
|
544
618
|
return
|
|
545
619
|
}
|
|
620
|
+
|
|
546
621
|
val metadata = callMetadata.remove(callId)
|
|
547
622
|
activeCalls.remove(callId)
|
|
623
|
+
|
|
548
624
|
stopRingback()
|
|
549
625
|
stopRingtone()
|
|
550
626
|
cancelIncomingCallUI()
|
|
627
|
+
|
|
551
628
|
if (currentCallId == callId) {
|
|
552
|
-
currentCallId =
|
|
629
|
+
currentCallId =
|
|
630
|
+
activeCalls.filter { it.value.state != CallState.ENDED }.keys.firstOrNull()
|
|
553
631
|
}
|
|
632
|
+
|
|
554
633
|
val context = requireContext()
|
|
555
634
|
val closeActivityIntent = Intent("com.qusaieilouti99.callmanager.CLOSE_CALL_ACTIVITY")
|
|
556
635
|
.setPackage(context.packageName)
|
|
557
636
|
.putExtra("callId", callId)
|
|
637
|
+
|
|
558
638
|
try {
|
|
559
639
|
context.sendBroadcast(closeActivityIntent)
|
|
560
640
|
Log.d(TAG, "Sent close broadcast for CallActivity: $callId")
|
|
561
641
|
} catch (e: Exception) {
|
|
562
642
|
Log.w(TAG, "Failed to send close broadcast: ${e.message}")
|
|
563
643
|
}
|
|
644
|
+
|
|
564
645
|
telecomConnections[callId]?.let { connection ->
|
|
565
646
|
connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
566
647
|
connection.destroy()
|
|
567
648
|
removeTelecomConnection(callId)
|
|
568
649
|
}
|
|
650
|
+
|
|
569
651
|
if (activeCalls.isEmpty()) {
|
|
570
652
|
cleanup()
|
|
571
653
|
} else {
|
|
572
654
|
updateForegroundNotification()
|
|
573
655
|
}
|
|
656
|
+
|
|
574
657
|
updateLockScreenBypass()
|
|
658
|
+
|
|
575
659
|
for (listener in callEndListeners) {
|
|
576
660
|
mainHandler.post {
|
|
577
|
-
try {
|
|
661
|
+
try {
|
|
662
|
+
listener.onCallEnded(callId)
|
|
663
|
+
} catch (_: Throwable) {
|
|
664
|
+
// swallow
|
|
665
|
+
}
|
|
578
666
|
}
|
|
579
667
|
}
|
|
668
|
+
|
|
580
669
|
emitEvent(CallEventType.CALL_ENDED, JSONObject().apply {
|
|
581
670
|
put("callId", callId)
|
|
582
671
|
metadata?.let {
|
|
583
|
-
try { put("metadata", JSONObject(it)) }
|
|
672
|
+
try { put("metadata", JSONObject(it)) }
|
|
673
|
+
catch (e: Exception) { put("metadata", it) }
|
|
584
674
|
}
|
|
585
675
|
})
|
|
586
676
|
}
|
|
587
677
|
|
|
678
|
+
// ====== NEW TELECOM-DRIVEN AUDIO ROUTING SYSTEM ======
|
|
679
|
+
|
|
588
680
|
fun getAudioDevices(): AudioRoutesInfo {
|
|
589
681
|
val context = requireContext()
|
|
590
|
-
|
|
682
|
+
val am = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
591
683
|
?: return AudioRoutesInfo(emptyArray(), "Unknown")
|
|
684
|
+
|
|
592
685
|
val devices = mutableSetOf<String>()
|
|
593
686
|
devices.add("Earpiece")
|
|
594
687
|
devices.add("Speaker")
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
688
|
+
|
|
689
|
+
val outputDevices = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
|
690
|
+
for (device in outputDevices) {
|
|
691
|
+
when (device.type) {
|
|
692
|
+
android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
|
|
693
|
+
android.media.AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
|
|
694
|
+
android.media.AudioDeviceInfo.TYPE_BLE_HEADSET -> devices.add("Bluetooth")
|
|
695
|
+
android.media.AudioDeviceInfo.TYPE_WIRED_HEADPHONES,
|
|
696
|
+
android.media.AudioDeviceInfo.TYPE_WIRED_HEADSET,
|
|
697
|
+
android.media.AudioDeviceInfo.TYPE_USB_HEADSET -> devices.add("Headset")
|
|
604
698
|
}
|
|
605
|
-
} else {
|
|
606
|
-
@Suppress("DEPRECATION")
|
|
607
|
-
if (audioManager?.isBluetoothA2dpOn == true || audioManager?.isBluetoothScoOn == true) { devices.add("Bluetooth") }
|
|
608
|
-
@Suppress("DEPRECATION")
|
|
609
|
-
if (audioManager?.isWiredHeadsetOn == true) { devices.add("Headset") }
|
|
610
699
|
}
|
|
611
|
-
Log.d(TAG, "Available audio devices: ${devices.toList()}, current: $currentAudioRoute")
|
|
612
|
-
val deviceHolders = devices.map { StringHolder(it) }.toTypedArray()
|
|
613
|
-
return AudioRoutesInfo(deviceHolders, currentAudioRoute)
|
|
614
|
-
}
|
|
615
700
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
CallAudioState.ROUTE_WIRED_HEADSET -> "Headset"
|
|
623
|
-
else -> "Unknown"
|
|
624
|
-
}
|
|
625
|
-
if (currentAudioRoute != routeString) {
|
|
626
|
-
Log.d(TAG, "Audio route has changed. New route: $routeString")
|
|
627
|
-
currentAudioRoute = routeString
|
|
628
|
-
emitAudioRouteChanged(currentAudioRoute)
|
|
629
|
-
}
|
|
701
|
+
val currentRoute = (telecomConnections[currentCallId] as? MyConnection)
|
|
702
|
+
?.getCurrentRouteString() ?: "Unknown"
|
|
703
|
+
|
|
704
|
+
Log.d(TAG, "getAudioDevices: Available=${devices.toList()}, Current=$currentRoute")
|
|
705
|
+
val deviceHolders = devices.map { StringHolder(it) }.toTypedArray()
|
|
706
|
+
return AudioRoutesInfo(deviceHolders, currentRoute)
|
|
630
707
|
}
|
|
631
708
|
|
|
632
709
|
fun setAudioRoute(route: String) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
"Bluetooth" -> CallAudioState.ROUTE_BLUETOOTH
|
|
638
|
-
"Headset" -> CallAudioState.ROUTE_WIRED_HEADSET
|
|
639
|
-
else -> {
|
|
640
|
-
Log.w(TAG, "Unknown audio route requested: $route")
|
|
641
|
-
return
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
currentCallId?.let { callId ->
|
|
645
|
-
val connection = telecomConnections[callId] as? MyConnection
|
|
646
|
-
if (connection != null) {
|
|
647
|
-
Log.d(TAG, "Requesting audio route change via MyConnection for call $callId")
|
|
648
|
-
connection.requestAudioRoute(telecomRoute)
|
|
649
|
-
} else {
|
|
650
|
-
Log.w(TAG, "Could not find MyConnection for active call $callId to set audio route.")
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
private fun isWiredHeadsetConnected(): Boolean {
|
|
656
|
-
val am = audioManager ?: return false
|
|
657
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
658
|
-
val devices = am.getDevices(AudioManager.GET_DEVICES_ALL)
|
|
659
|
-
return devices.any { it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET || it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || it.type == AudioDeviceInfo.TYPE_USB_HEADSET }
|
|
660
|
-
} else {
|
|
661
|
-
@Suppress("DEPRECATION")
|
|
662
|
-
return am.isWiredHeadsetOn
|
|
710
|
+
Log.d(TAG, "setAudioRoute requested for: $route")
|
|
711
|
+
val connection = telecomConnections[currentCallId] as? MyConnection ?: run {
|
|
712
|
+
Log.w(TAG, "Cannot set audio route, no active connection found for callId: $currentCallId")
|
|
713
|
+
return
|
|
663
714
|
}
|
|
664
|
-
}
|
|
665
715
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
716
|
+
val telecomRoute = when (route) {
|
|
717
|
+
"Speaker" -> CallAudioState.ROUTE_SPEAKER
|
|
718
|
+
"Earpiece" -> CallAudioState.ROUTE_EARPIECE
|
|
719
|
+
"Bluetooth" -> CallAudioState.ROUTE_BLUETOOTH
|
|
720
|
+
"Headset" -> CallAudioState.ROUTE_WIRED_HEADSET
|
|
721
|
+
else -> {
|
|
722
|
+
Log.w(TAG, "Unknown audio route string: $route")
|
|
723
|
+
return
|
|
724
|
+
}
|
|
674
725
|
}
|
|
726
|
+
connection.requestAudioRouteChange(telecomRoute)
|
|
675
727
|
}
|
|
676
728
|
|
|
677
729
|
/**
|
|
678
|
-
*
|
|
730
|
+
* Called by MyConnection when the audio route changes. This is the single
|
|
731
|
+
* source of truth for route updates.
|
|
732
|
+
*/
|
|
733
|
+
fun onTelecomAudioRouteChanged(callId: String, newRoute: String) {
|
|
734
|
+
Log.d(TAG, "onTelecomAudioRouteChanged for callId $callId, new route: $newRoute")
|
|
735
|
+
emitAudioRouteChanged()
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Called by MyConnection when it becomes active to set the initial, logical
|
|
740
|
+
* audio route.
|
|
679
741
|
*/
|
|
680
742
|
fun setInitialAudioRouteForCall(callId: String, callType: String) {
|
|
681
|
-
|
|
743
|
+
val am = audioManager ?: return
|
|
744
|
+
val connection = telecomConnections[callId] as? MyConnection ?: return
|
|
745
|
+
|
|
746
|
+
// Determine default route based on Android standards
|
|
682
747
|
val defaultRoute = when {
|
|
683
|
-
|
|
684
|
-
|
|
748
|
+
connection.isBluetoothAvailable() -> "Bluetooth"
|
|
749
|
+
am.isWiredHeadsetOn -> "Headset"
|
|
685
750
|
callType.equals("Video", ignoreCase = true) -> "Speaker"
|
|
686
751
|
else -> "Earpiece"
|
|
687
752
|
}
|
|
688
|
-
Log.d(TAG, "Requesting initial audio route to be: $defaultRoute")
|
|
689
|
-
setAudioRoute(defaultRoute)
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
private fun setAudioMode() {
|
|
693
|
-
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
694
|
-
Log.d(TAG, "Audio mode set to MODE_IN_COMMUNICATION")
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
private fun resetAudioMode() {
|
|
698
|
-
if (activeCalls.isEmpty()) {
|
|
699
|
-
audioManager?.mode = AudioManager.MODE_NORMAL
|
|
700
|
-
currentAudioRoute = "Unknown"
|
|
701
|
-
unregisterAudioDeviceCallback()
|
|
702
|
-
Log.d(TAG, "Audio mode reset to MODE_NORMAL")
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
753
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
val deviceStrings = info.devices.map { it.value }
|
|
709
|
-
val payload = JSONObject().apply {
|
|
710
|
-
put("devices", JSONArray(deviceStrings))
|
|
711
|
-
put("currentRoute", currentRoute)
|
|
712
|
-
}
|
|
713
|
-
emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, payload)
|
|
714
|
-
Log.d(TAG, "Emitted AUDIO_ROUTE_CHANGED: $currentRoute, available: $deviceStrings")
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
private val audioDeviceCallback = object : AudioDeviceCallback() {
|
|
718
|
-
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
|
|
719
|
-
Log.d(TAG, "Audio devices added, emitting change event.")
|
|
720
|
-
emitAudioDevicesChanged()
|
|
721
|
-
}
|
|
722
|
-
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
|
|
723
|
-
Log.d(TAG, "Audio devices removed, emitting change event.")
|
|
724
|
-
emitAudioDevicesChanged()
|
|
725
|
-
}
|
|
754
|
+
Log.d(TAG, "Requesting initial audio route for call $callId: $defaultRoute")
|
|
755
|
+
setAudioRoute(defaultRoute)
|
|
726
756
|
}
|
|
727
757
|
|
|
728
|
-
private fun
|
|
758
|
+
private fun emitAudioRouteChanged() {
|
|
729
759
|
val info = getAudioDevices()
|
|
730
760
|
val deviceStrings = info.devices.map { it.value }
|
|
731
761
|
val payload = JSONObject().apply {
|
|
732
762
|
put("devices", JSONArray(deviceStrings))
|
|
733
763
|
put("currentRoute", info.currentRoute)
|
|
734
764
|
}
|
|
735
|
-
emitEvent(CallEventType.
|
|
736
|
-
Log.d(TAG, "Emitted
|
|
765
|
+
emitEvent(CallEventType.AUDIO_ROUTE_CHANGED, payload)
|
|
766
|
+
Log.d(TAG, "Emitted AUDIO_ROUTE_CHANGED: current=${info.currentRoute}, available=$deviceStrings")
|
|
737
767
|
}
|
|
738
768
|
|
|
739
|
-
|
|
740
|
-
if (isCallActive()) {
|
|
741
|
-
val context = requireContext()
|
|
742
|
-
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
743
|
-
try {
|
|
744
|
-
audioManager?.registerAudioDeviceCallback(audioDeviceCallback, mainHandler)
|
|
745
|
-
Log.d(TAG, "Audio device callback registered")
|
|
746
|
-
} catch (e: Exception) {
|
|
747
|
-
Log.w(TAG, "Failed to register audio device callback: ${e.message}")
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
fun unregisterAudioDeviceCallback() {
|
|
753
|
-
try {
|
|
754
|
-
audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
|
|
755
|
-
Log.d(TAG, "Audio device callback unregistered")
|
|
756
|
-
} catch (e: Exception) {
|
|
757
|
-
Log.w(TAG, "Failed to unregister audio device callback: ${e.message}")
|
|
758
|
-
}
|
|
759
|
-
}
|
|
769
|
+
// ====== END AUDIO ROUTING SYSTEM ======
|
|
760
770
|
|
|
761
771
|
fun keepScreenAwake(keepAwake: Boolean) {
|
|
762
772
|
val context = requireContext()
|
|
@@ -791,7 +801,6 @@ object CallEngine {
|
|
|
791
801
|
}
|
|
792
802
|
|
|
793
803
|
private fun validateOutgoingCallRequest(): Boolean {
|
|
794
|
-
if (canMakeMultipleCalls) return true
|
|
795
804
|
return !activeCalls.any {
|
|
796
805
|
it.value.state == CallState.INCOMING || it.value.state == CallState.ACTIVE
|
|
797
806
|
}
|
|
@@ -802,37 +811,33 @@ object CallEngine {
|
|
|
802
811
|
put("callId", callId)
|
|
803
812
|
put("reason", reason)
|
|
804
813
|
})
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
814
|
+
|
|
815
|
+
// Only remove metadata if there's NO existing active call with this ID
|
|
816
|
+
val existingCall = activeCalls[callId]
|
|
817
|
+
if (existingCall == null) {
|
|
818
|
+
callMetadata.remove(callId)
|
|
819
|
+
Log.d(TAG, "Removed metadata for rejected call $callId (no existing call)")
|
|
820
|
+
} else {
|
|
821
|
+
Log.d(TAG, "Kept metadata for callId: $callId (existing call: ${existingCall.state})")
|
|
822
|
+
}
|
|
813
823
|
}
|
|
814
824
|
|
|
815
825
|
private fun createNotificationChannel() {
|
|
816
826
|
val context = requireContext()
|
|
817
827
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
818
|
-
val channel = NotificationChannel(
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
.build()
|
|
832
|
-
)
|
|
833
|
-
} else {
|
|
834
|
-
channel.setSound(null, null)
|
|
835
|
-
channel.importance = NotificationManager.IMPORTANCE_HIGH
|
|
828
|
+
val channel = NotificationChannel(
|
|
829
|
+
NOTIF_CHANNEL_ID,
|
|
830
|
+
"Incoming Call Channel",
|
|
831
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
832
|
+
).apply {
|
|
833
|
+
description = "Notifications for incoming calls"
|
|
834
|
+
enableLights(true)
|
|
835
|
+
lightColor = Color.GREEN
|
|
836
|
+
enableVibration(true)
|
|
837
|
+
setBypassDnd(true)
|
|
838
|
+
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
839
|
+
// Sound is handled by the RingtoneManager directly for better control
|
|
840
|
+
setSound(null, null)
|
|
836
841
|
}
|
|
837
842
|
val manager = context.getSystemService(NotificationManager::class.java)
|
|
838
843
|
manager.createNotificationChannel(channel)
|
|
@@ -842,36 +847,37 @@ object CallEngine {
|
|
|
842
847
|
private fun showIncomingCallUI(callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
843
848
|
val context = requireContext()
|
|
844
849
|
Log.d(TAG, "Showing incoming call UI for $callId")
|
|
850
|
+
|
|
845
851
|
val useCallStyleNotification = supportsCallStyleNotifications()
|
|
846
852
|
Log.d(TAG, "Using CallStyle notification: $useCallStyleNotification")
|
|
847
|
-
|
|
848
|
-
|
|
853
|
+
|
|
854
|
+
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
855
|
+
if (keyguardManager.isKeyguardLocked || !useCallStyleNotification) {
|
|
856
|
+
Log.d(TAG, "Device is locked or CallStyle not supported - using full-screen CallActivity.")
|
|
849
857
|
showCallActivityOverlay(context, callId, callerName, callType, callerPicUrl)
|
|
850
858
|
} else {
|
|
851
|
-
Log.d(TAG, "Device is unlocked and supports CallStyle - using
|
|
859
|
+
Log.d(TAG, "Device is unlocked and supports CallStyle - using heads-up notification.")
|
|
852
860
|
showStandardNotification(context, callId, callerName, callType, callerPicUrl)
|
|
853
861
|
}
|
|
854
862
|
playRingtone()
|
|
855
863
|
}
|
|
856
864
|
|
|
857
|
-
private fun isDeviceLocked(context: Context): Boolean {
|
|
858
|
-
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
859
|
-
return keyguardManager.isKeyguardLocked
|
|
860
|
-
}
|
|
861
|
-
|
|
862
865
|
private fun showCallActivityOverlay(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
863
866
|
val overlayIntent = Intent(context, CallActivity::class.java).apply {
|
|
864
|
-
addFlags(
|
|
867
|
+
addFlags(
|
|
868
|
+
Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
869
|
+
Intent.FLAG_ACTIVITY_CLEAR_TASK or
|
|
870
|
+
Intent.FLAG_ACTIVITY_NO_ANIMATION or
|
|
871
|
+
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
|
872
|
+
)
|
|
865
873
|
putExtra("callId", callId)
|
|
866
874
|
putExtra("callerName", callerName)
|
|
867
875
|
putExtra("callType", callType)
|
|
868
876
|
callerPicUrl?.let { putExtra("callerAvatar", it) }
|
|
869
877
|
putExtra("LOCK_SCREEN_MODE", true)
|
|
870
878
|
}
|
|
879
|
+
|
|
871
880
|
try {
|
|
872
|
-
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
|
873
|
-
val wakeLock = powerManager.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP, "CallEngine:LockScreenWake")
|
|
874
|
-
wakeLock.acquire(5000)
|
|
875
881
|
context.startActivity(overlayIntent)
|
|
876
882
|
Log.d(TAG, "Successfully launched CallActivity overlay")
|
|
877
883
|
} catch (e: Exception) {
|
|
@@ -883,6 +889,7 @@ object CallEngine {
|
|
|
883
889
|
private fun showStandardNotification(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
884
890
|
createNotificationChannel()
|
|
885
891
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
892
|
+
|
|
886
893
|
val fullScreenIntent = Intent(context, CallActivity::class.java).apply {
|
|
887
894
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
888
895
|
putExtra("callId", callId)
|
|
@@ -890,48 +897,84 @@ object CallEngine {
|
|
|
890
897
|
putExtra("callType", callType)
|
|
891
898
|
callerPicUrl?.let { putExtra("callerAvatar", it) }
|
|
892
899
|
}
|
|
893
|
-
|
|
900
|
+
|
|
901
|
+
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
902
|
+
context, callId.hashCode(), fullScreenIntent,
|
|
903
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
904
|
+
)
|
|
905
|
+
|
|
894
906
|
val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
|
|
895
907
|
action = "com.qusaieilouti99.callmanager.ANSWER_CALL"
|
|
896
908
|
putExtra("callId", callId)
|
|
897
909
|
}
|
|
898
|
-
val answerPendingIntent = PendingIntent.getBroadcast(
|
|
910
|
+
val answerPendingIntent = PendingIntent.getBroadcast(
|
|
911
|
+
context, 0, answerIntent,
|
|
912
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
913
|
+
)
|
|
914
|
+
|
|
899
915
|
val declineIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
|
|
900
916
|
action = "com.qusaieilouti99.callmanager.DECLINE_CALL"
|
|
901
917
|
putExtra("callId", callId)
|
|
902
918
|
}
|
|
903
|
-
val declinePendingIntent = PendingIntent.getBroadcast(
|
|
919
|
+
val declinePendingIntent = PendingIntent.getBroadcast(
|
|
920
|
+
context, 1, declineIntent,
|
|
921
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
922
|
+
)
|
|
923
|
+
|
|
904
924
|
val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && supportsCallStyleNotifications()) {
|
|
905
|
-
val person = android.app.Person.Builder()
|
|
925
|
+
val person = android.app.Person.Builder()
|
|
926
|
+
.setName(callerName)
|
|
927
|
+
.setImportant(true)
|
|
928
|
+
.build()
|
|
906
929
|
Notification.Builder(context, NOTIF_CHANNEL_ID)
|
|
907
930
|
.setSmallIcon(android.R.drawable.sym_call_incoming)
|
|
908
|
-
.setStyle(
|
|
931
|
+
.setStyle(
|
|
932
|
+
Notification.CallStyle.forIncomingCall(
|
|
933
|
+
person,
|
|
934
|
+
declinePendingIntent,
|
|
935
|
+
answerPendingIntent
|
|
936
|
+
)
|
|
937
|
+
)
|
|
909
938
|
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
910
|
-
.setOngoing(true)
|
|
911
|
-
.
|
|
939
|
+
.setOngoing(true)
|
|
940
|
+
.setCategory(Notification.CATEGORY_CALL)
|
|
941
|
+
.setVisibility(Notification.VISIBILITY_PUBLIC)
|
|
942
|
+
.build()
|
|
912
943
|
} else {
|
|
913
944
|
Notification.Builder(context, NOTIF_CHANNEL_ID)
|
|
914
945
|
.setSmallIcon(android.R.drawable.sym_call_incoming)
|
|
915
|
-
.setContentTitle("Incoming Call")
|
|
916
|
-
.
|
|
946
|
+
.setContentTitle("Incoming Call")
|
|
947
|
+
.setContentText(callerName)
|
|
948
|
+
.setPriority(Notification.PRIORITY_MAX)
|
|
949
|
+
.setCategory(Notification.CATEGORY_CALL)
|
|
917
950
|
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
918
951
|
.addAction(android.R.drawable.sym_action_call, "Answer", answerPendingIntent)
|
|
919
952
|
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Decline", declinePendingIntent)
|
|
920
|
-
.setOngoing(true)
|
|
953
|
+
.setOngoing(true)
|
|
954
|
+
.setVisibility(Notification.VISIBILITY_PUBLIC)
|
|
955
|
+
.build()
|
|
921
956
|
}
|
|
957
|
+
|
|
922
958
|
notificationManager.notify(NOTIF_ID, notification)
|
|
923
959
|
}
|
|
924
960
|
|
|
925
961
|
fun cancelIncomingCallUI() {
|
|
926
962
|
val context = requireContext()
|
|
927
|
-
val notificationManager =
|
|
963
|
+
val notificationManager =
|
|
964
|
+
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
928
965
|
notificationManager.cancel(NOTIF_ID)
|
|
929
966
|
stopRingtone()
|
|
930
967
|
}
|
|
931
968
|
|
|
932
969
|
private fun startForegroundService() {
|
|
933
970
|
val context = requireContext()
|
|
934
|
-
val currentCall = activeCalls.values.find {
|
|
971
|
+
val currentCall = activeCalls.values.find {
|
|
972
|
+
it.state == CallState.ACTIVE ||
|
|
973
|
+
it.state == CallState.INCOMING ||
|
|
974
|
+
it.state == CallState.DIALING ||
|
|
975
|
+
it.state == CallState.HELD
|
|
976
|
+
}
|
|
977
|
+
|
|
935
978
|
val intent = Intent(context, CallForegroundService::class.java)
|
|
936
979
|
currentCall?.let {
|
|
937
980
|
intent.putExtra("callId", it.callId)
|
|
@@ -939,6 +982,7 @@ object CallEngine {
|
|
|
939
982
|
intent.putExtra("displayName", it.displayName)
|
|
940
983
|
intent.putExtra("state", it.state.name)
|
|
941
984
|
}
|
|
985
|
+
|
|
942
986
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
943
987
|
context.startForegroundService(intent)
|
|
944
988
|
} else {
|
|
@@ -958,28 +1002,16 @@ object CallEngine {
|
|
|
958
1002
|
|
|
959
1003
|
private fun isMainActivityInForeground(): Boolean {
|
|
960
1004
|
val context = requireContext()
|
|
961
|
-
val activityManager =
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
}
|
|
969
|
-
} catch (e: Exception) {
|
|
970
|
-
Log.w(TAG, "Failed to get app tasks: ${e.message}")
|
|
971
|
-
}
|
|
972
|
-
} else {
|
|
973
|
-
try {
|
|
974
|
-
@Suppress("DEPRECATION")
|
|
975
|
-
val tasks = activityManager.getRunningTasks(1)
|
|
976
|
-
if (tasks.isNotEmpty()) {
|
|
977
|
-
val runningTaskInfo = tasks[0]
|
|
978
|
-
return runningTaskInfo.topActivity?.className?.contains("MainActivity") == true
|
|
979
|
-
}
|
|
980
|
-
} catch (e: Exception) {
|
|
981
|
-
Log.w(TAG, "Failed to get running tasks: ${e.message}")
|
|
1005
|
+
val activityManager =
|
|
1006
|
+
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
1007
|
+
try {
|
|
1008
|
+
val tasks = activityManager.appTasks
|
|
1009
|
+
if (tasks.isNotEmpty()) {
|
|
1010
|
+
val taskInfo = tasks[0].taskInfo
|
|
1011
|
+
return taskInfo.topActivity?.className?.contains("MainActivity") == true
|
|
982
1012
|
}
|
|
1013
|
+
} catch (e: Exception) {
|
|
1014
|
+
Log.w(TAG, "Failed to get app tasks: ${e.message}")
|
|
983
1015
|
}
|
|
984
1016
|
return false
|
|
985
1017
|
}
|
|
@@ -989,17 +1021,23 @@ object CallEngine {
|
|
|
989
1021
|
Log.d(TAG, "MainActivity is already in foreground, skipping")
|
|
990
1022
|
return
|
|
991
1023
|
}
|
|
1024
|
+
|
|
992
1025
|
Log.d(TAG, "Bringing app to foreground")
|
|
993
1026
|
val context = requireContext()
|
|
994
1027
|
val packageName = context.packageName
|
|
995
1028
|
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
|
996
|
-
launchIntent?.addFlags(
|
|
1029
|
+
launchIntent?.addFlags(
|
|
1030
|
+
Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
1031
|
+
Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
|
1032
|
+
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
1033
|
+
)
|
|
1034
|
+
|
|
997
1035
|
if (isCallActive()) {
|
|
998
1036
|
launchIntent?.putExtra("BYPASS_LOCK_SCREEN", true)
|
|
999
1037
|
}
|
|
1038
|
+
|
|
1000
1039
|
try {
|
|
1001
1040
|
context.startActivity(launchIntent)
|
|
1002
|
-
Handler(Looper.getMainLooper()).postDelayed({ updateLockScreenBypass() }, 100)
|
|
1003
1041
|
} catch (e: Exception) {
|
|
1004
1042
|
Log.e(TAG, "Failed to bring app to foreground: ${e.message}")
|
|
1005
1043
|
}
|
|
@@ -1007,12 +1045,15 @@ object CallEngine {
|
|
|
1007
1045
|
|
|
1008
1046
|
private fun registerPhoneAccount() {
|
|
1009
1047
|
val context = requireContext()
|
|
1010
|
-
val telecomManager =
|
|
1048
|
+
val telecomManager =
|
|
1049
|
+
context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
1011
1050
|
val phoneAccountHandle = getPhoneAccountHandle()
|
|
1051
|
+
|
|
1012
1052
|
if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
|
|
1013
1053
|
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
|
|
1014
1054
|
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
|
|
1015
1055
|
.build()
|
|
1056
|
+
|
|
1016
1057
|
try {
|
|
1017
1058
|
telecomManager.registerPhoneAccount(phoneAccount)
|
|
1018
1059
|
Log.d(TAG, "PhoneAccount registered successfully")
|
|
@@ -1024,13 +1065,16 @@ object CallEngine {
|
|
|
1024
1065
|
|
|
1025
1066
|
private fun getPhoneAccountHandle(): PhoneAccountHandle {
|
|
1026
1067
|
val context = requireContext()
|
|
1027
|
-
return PhoneAccountHandle(
|
|
1068
|
+
return PhoneAccountHandle(
|
|
1069
|
+
ComponentName(context, MyConnectionService::class.java),
|
|
1070
|
+
PHONE_ACCOUNT_ID
|
|
1071
|
+
)
|
|
1028
1072
|
}
|
|
1029
1073
|
|
|
1030
1074
|
private fun playRingtone() {
|
|
1031
1075
|
val context = requireContext()
|
|
1032
|
-
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
1033
1076
|
audioManager?.mode = AudioManager.MODE_RINGTONE
|
|
1077
|
+
|
|
1034
1078
|
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
|
1035
1079
|
vibrator?.let { v ->
|
|
1036
1080
|
val pattern = longArrayOf(0L, 500L, 500L)
|
|
@@ -1041,6 +1085,7 @@ object CallEngine {
|
|
|
1041
1085
|
v.vibrate(pattern, 0)
|
|
1042
1086
|
}
|
|
1043
1087
|
}
|
|
1088
|
+
|
|
1044
1089
|
try {
|
|
1045
1090
|
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
1046
1091
|
ringtone = RingtoneManager.getRingtone(context, uri)
|
|
@@ -1059,6 +1104,7 @@ object CallEngine {
|
|
|
1059
1104
|
Log.e(TAG, "Error stopping ringtone", e)
|
|
1060
1105
|
}
|
|
1061
1106
|
ringtone = null
|
|
1107
|
+
|
|
1062
1108
|
vibrator?.cancel()
|
|
1063
1109
|
vibrator = null
|
|
1064
1110
|
}
|
|
@@ -1066,8 +1112,10 @@ object CallEngine {
|
|
|
1066
1112
|
private fun startRingback() {
|
|
1067
1113
|
val context = requireContext()
|
|
1068
1114
|
if (ringbackPlayer?.isPlaying == true) return
|
|
1115
|
+
|
|
1069
1116
|
try {
|
|
1070
|
-
val ringbackUri =
|
|
1117
|
+
val ringbackUri =
|
|
1118
|
+
Uri.parse("android.resource://${context.packageName}/raw/ringback_tone")
|
|
1071
1119
|
ringbackPlayer = MediaPlayer.create(context, ringbackUri)
|
|
1072
1120
|
ringbackPlayer?.apply {
|
|
1073
1121
|
isLooping = true
|
|
@@ -1093,14 +1141,15 @@ object CallEngine {
|
|
|
1093
1141
|
Log.d(TAG, "Performing cleanup")
|
|
1094
1142
|
stopForegroundService()
|
|
1095
1143
|
keepScreenAwake(false)
|
|
1096
|
-
|
|
1144
|
+
// Reset audio mode via AudioManager when all calls are truly gone
|
|
1145
|
+
audioManager?.mode = AudioManager.MODE_NORMAL
|
|
1097
1146
|
}
|
|
1098
1147
|
|
|
1099
1148
|
fun onApplicationTerminate() {
|
|
1100
1149
|
Log.d(TAG, "Application terminating")
|
|
1101
1150
|
activeCalls.keys.toList().forEach { callId ->
|
|
1102
1151
|
telecomConnections[callId]?.let { conn ->
|
|
1103
|
-
conn.setDisconnected(DisconnectCause(DisconnectCause.
|
|
1152
|
+
conn.setDisconnected(DisconnectCause(DisconnectCause.OTHER))
|
|
1104
1153
|
conn.destroy()
|
|
1105
1154
|
}
|
|
1106
1155
|
}
|