@qusaieilouti99/call-manager 0.1.145 → 0.1.147
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.
|
@@ -44,6 +44,10 @@ object CallEngine {
|
|
|
44
44
|
fun onCallEnded(callId: String)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
interface LockScreenBypassCallback {
|
|
48
|
+
fun onLockScreenBypassChanged(shouldBypass: Boolean)
|
|
49
|
+
}
|
|
50
|
+
|
|
47
51
|
private val callEndListeners = CopyOnWriteArrayList<CallEndListener>()
|
|
48
52
|
private val mainHandler = Handler(Looper.getMainLooper())
|
|
49
53
|
|
|
@@ -75,16 +79,18 @@ object CallEngine {
|
|
|
75
79
|
private var eventHandler: ((CallEventType, String) -> Unit)? = null
|
|
76
80
|
private val cachedEvents = mutableListOf<Pair<CallEventType, String>>()
|
|
77
81
|
|
|
78
|
-
// --- Modern Audio Management State ---
|
|
79
82
|
private var userSelectedAudioRoute: String? = null
|
|
80
83
|
private val audioStateLock = Any()
|
|
81
84
|
|
|
85
|
+
private var lockScreenBypassActive = false
|
|
86
|
+
private val lockScreenBypassCallbacks = mutableSetOf<LockScreenBypassCallback>()
|
|
87
|
+
|
|
82
88
|
fun initialize(context: Context) {
|
|
83
89
|
synchronized(initializationLock) {
|
|
84
90
|
if (isInitialized.compareAndSet(false, true)) {
|
|
85
91
|
appContext = context.applicationContext
|
|
86
92
|
audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
87
|
-
registerAudioDeviceCallback()
|
|
93
|
+
registerAudioDeviceCallback()
|
|
88
94
|
Log.d(TAG, "CallEngine initialized successfully")
|
|
89
95
|
}
|
|
90
96
|
}
|
|
@@ -135,6 +141,31 @@ object CallEngine {
|
|
|
135
141
|
stopRingtone()
|
|
136
142
|
}
|
|
137
143
|
|
|
144
|
+
fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
|
|
145
|
+
lockScreenBypassCallbacks.add(callback)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fun unregisterLockScreenBypassCallback(callback: LockScreenBypassCallback) {
|
|
149
|
+
lockScreenBypassCallbacks.remove(callback)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private fun updateLockScreenBypass() {
|
|
153
|
+
val shouldBypass = isCallActive()
|
|
154
|
+
if (lockScreenBypassActive != shouldBypass) {
|
|
155
|
+
lockScreenBypassActive = shouldBypass
|
|
156
|
+
Log.d(TAG, "Lock screen bypass state changed: $lockScreenBypassActive")
|
|
157
|
+
lockScreenBypassCallbacks.forEach { callback ->
|
|
158
|
+
try {
|
|
159
|
+
callback.onLockScreenBypassChanged(shouldBypass)
|
|
160
|
+
} catch (e: Exception) {
|
|
161
|
+
Log.w(TAG, "Error notifying lock screen bypass callback", e)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fun isLockScreenBypassActive(): Boolean = lockScreenBypassActive
|
|
168
|
+
|
|
138
169
|
fun addTelecomConnection(callId: String, connection: Connection) {
|
|
139
170
|
telecomConnections[callId] = connection
|
|
140
171
|
}
|
|
@@ -192,6 +223,7 @@ object CallEngine {
|
|
|
192
223
|
Log.e(TAG, "Failed to report incoming call: ${e.message}", e)
|
|
193
224
|
endCallInternal(callId)
|
|
194
225
|
}
|
|
226
|
+
updateLockScreenBypass()
|
|
195
227
|
}
|
|
196
228
|
|
|
197
229
|
fun startOutgoingCall(
|
|
@@ -240,6 +272,7 @@ object CallEngine {
|
|
|
240
272
|
Log.e(TAG, "Failed to start outgoing call: ${e.message}", e)
|
|
241
273
|
endCallInternal(callId)
|
|
242
274
|
}
|
|
275
|
+
updateLockScreenBypass()
|
|
243
276
|
}
|
|
244
277
|
|
|
245
278
|
fun startCall(
|
|
@@ -278,6 +311,7 @@ object CallEngine {
|
|
|
278
311
|
put("callType", callType)
|
|
279
312
|
put("displayName", targetName)
|
|
280
313
|
})
|
|
314
|
+
updateLockScreenBypass()
|
|
281
315
|
}
|
|
282
316
|
|
|
283
317
|
fun callAnsweredFromJS(callId: String) {
|
|
@@ -303,11 +337,9 @@ object CallEngine {
|
|
|
303
337
|
startForegroundService()
|
|
304
338
|
keepScreenAwake(true)
|
|
305
339
|
|
|
306
|
-
// Set initial audio route using the new robust system
|
|
307
340
|
synchronized(audioStateLock) {
|
|
308
|
-
userSelectedAudioRoute = null
|
|
341
|
+
userSelectedAudioRoute = null
|
|
309
342
|
}
|
|
310
|
-
// Give the audio system a moment to initialize after the call becomes active
|
|
311
343
|
mainHandler.postDelayed({
|
|
312
344
|
updateAndApplyAudioRoute()
|
|
313
345
|
}, 300)
|
|
@@ -315,6 +347,7 @@ object CallEngine {
|
|
|
315
347
|
if (isLocalAnswer) {
|
|
316
348
|
emitCallAnsweredWithMetadata(callId)
|
|
317
349
|
}
|
|
350
|
+
updateLockScreenBypass()
|
|
318
351
|
}
|
|
319
352
|
|
|
320
353
|
private fun emitCallAnsweredWithMetadata(callId: String) {
|
|
@@ -345,6 +378,7 @@ object CallEngine {
|
|
|
345
378
|
telecomConnections[callId]?.setOnHold()
|
|
346
379
|
updateForegroundNotification()
|
|
347
380
|
emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
|
|
381
|
+
updateLockScreenBypass()
|
|
348
382
|
}
|
|
349
383
|
|
|
350
384
|
fun unholdCall(callId: String) {
|
|
@@ -357,6 +391,7 @@ object CallEngine {
|
|
|
357
391
|
telecomConnections[callId]?.setActive()
|
|
358
392
|
updateForegroundNotification()
|
|
359
393
|
emitEvent(CallEventType.CALL_UNHELD, JSONObject().put("callId", callId))
|
|
394
|
+
updateLockScreenBypass()
|
|
360
395
|
}
|
|
361
396
|
|
|
362
397
|
fun muteCall(callId: String) {
|
|
@@ -390,6 +425,7 @@ object CallEngine {
|
|
|
390
425
|
activeCalls.keys.toList().forEach { callId ->
|
|
391
426
|
endCallInternal(callId)
|
|
392
427
|
}
|
|
428
|
+
updateLockScreenBypass()
|
|
393
429
|
}
|
|
394
430
|
|
|
395
431
|
private fun endCallInternal(callId: String) {
|
|
@@ -414,6 +450,7 @@ object CallEngine {
|
|
|
414
450
|
|
|
415
451
|
callEndListeners.forEach { it.onCallEnded(callId) }
|
|
416
452
|
emitEvent(CallEventType.CALL_ENDED, JSONObject().put("callId", callId))
|
|
453
|
+
updateLockScreenBypass()
|
|
417
454
|
}
|
|
418
455
|
|
|
419
456
|
fun isCallActive(): Boolean = activeCalls.any {
|
|
@@ -423,10 +460,6 @@ object CallEngine {
|
|
|
423
460
|
it.value.state == CallState.HELD
|
|
424
461
|
}
|
|
425
462
|
|
|
426
|
-
/**
|
|
427
|
-
* Sets the user's desired audio route. This will be respected until a
|
|
428
|
-
* higher-priority device connects or this route becomes unavailable.
|
|
429
|
-
*/
|
|
430
463
|
fun setAudioRoute(route: String) {
|
|
431
464
|
synchronized(audioStateLock) {
|
|
432
465
|
Log.d(TAG, "User requested audio route: $route")
|
|
@@ -435,12 +468,16 @@ object CallEngine {
|
|
|
435
468
|
updateAndApplyAudioRoute()
|
|
436
469
|
}
|
|
437
470
|
|
|
438
|
-
/**
|
|
439
|
-
* Central function to determine and apply the correct audio route.
|
|
440
|
-
* This is the single source of truth for audio routing decisions.
|
|
441
|
-
*/
|
|
442
471
|
private fun updateAndApplyAudioRoute() {
|
|
443
|
-
|
|
472
|
+
// *** CRITICAL FIX ***
|
|
473
|
+
// Check for null currentCallId *before* using it as a key.
|
|
474
|
+
val callId = currentCallId
|
|
475
|
+
if (callId == null) {
|
|
476
|
+
Log.d(TAG, "Skipping audio route update: currentCallId is null.")
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
val call = activeCalls[callId]
|
|
444
481
|
if (call == null || call.state != CallState.ACTIVE) {
|
|
445
482
|
Log.d(TAG, "Skipping audio route update: No active call.")
|
|
446
483
|
return
|
|
@@ -450,11 +487,9 @@ object CallEngine {
|
|
|
450
487
|
val am = audioManager ?: return
|
|
451
488
|
val availableDevices = getAvailableAudioDevices()
|
|
452
489
|
|
|
453
|
-
// Determine the target route based on priority
|
|
454
490
|
val targetRoute = if (userSelectedAudioRoute != null && availableDevices.contains(userSelectedAudioRoute)) {
|
|
455
|
-
userSelectedAudioRoute!!
|
|
491
|
+
userSelectedAudioRoute!!
|
|
456
492
|
} else {
|
|
457
|
-
// Auto-select based on priority
|
|
458
493
|
when {
|
|
459
494
|
availableDevices.contains("Bluetooth") -> "Bluetooth"
|
|
460
495
|
availableDevices.contains("Headset") -> "Headset"
|
|
@@ -465,7 +500,6 @@ object CallEngine {
|
|
|
465
500
|
|
|
466
501
|
Log.d(TAG, "Updating audio route. Available: $availableDevices, User Pref: $userSelectedAudioRoute, Target: $targetRoute")
|
|
467
502
|
|
|
468
|
-
// Apply the target route
|
|
469
503
|
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
470
504
|
|
|
471
505
|
when (targetRoute) {
|
|
@@ -491,7 +525,6 @@ object CallEngine {
|
|
|
491
525
|
}
|
|
492
526
|
}
|
|
493
527
|
|
|
494
|
-
// After applying, emit the result
|
|
495
528
|
mainHandler.postDelayed({ emitAudioRouteChanged() }, 100)
|
|
496
529
|
}
|
|
497
530
|
}
|
|
@@ -499,8 +532,8 @@ object CallEngine {
|
|
|
499
532
|
private fun getAvailableAudioDevices(): Set<String> {
|
|
500
533
|
val am = audioManager ?: return emptySet()
|
|
501
534
|
val devices = mutableSetOf<String>()
|
|
502
|
-
devices.add("Earpiece")
|
|
503
|
-
devices.add("Speaker")
|
|
535
|
+
devices.add("Earpiece")
|
|
536
|
+
devices.add("Speaker")
|
|
504
537
|
|
|
505
538
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
506
539
|
val audioDevices = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
|
@@ -548,7 +581,6 @@ object CallEngine {
|
|
|
548
581
|
private val audioDeviceCallback = object : AudioDeviceCallback() {
|
|
549
582
|
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
|
|
550
583
|
Log.d(TAG, "Audio devices added. Triggering audio route update.")
|
|
551
|
-
// A new device was added, reset user preference to allow auto-switch to higher priority device
|
|
552
584
|
synchronized(audioStateLock) {
|
|
553
585
|
userSelectedAudioRoute = null
|
|
554
586
|
}
|
|
@@ -556,7 +588,6 @@ object CallEngine {
|
|
|
556
588
|
}
|
|
557
589
|
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
|
|
558
590
|
Log.d(TAG, "Audio devices removed. Triggering audio route update.")
|
|
559
|
-
// A device was removed, if it was the user's selection, clear it
|
|
560
591
|
synchronized(audioStateLock) {
|
|
561
592
|
val removedDeviceTypes = removedDevices.map {
|
|
562
593
|
when(it.type) {
|
|
@@ -581,7 +612,6 @@ object CallEngine {
|
|
|
581
612
|
Log.d(TAG, "Performing cleanup")
|
|
582
613
|
stopForegroundService()
|
|
583
614
|
keepScreenAwake(false)
|
|
584
|
-
// Reset audio state
|
|
585
615
|
synchronized(audioStateLock) {
|
|
586
616
|
userSelectedAudioRoute = null
|
|
587
617
|
}
|
|
@@ -601,7 +631,6 @@ object CallEngine {
|
|
|
601
631
|
description = "Notifications for incoming calls"
|
|
602
632
|
setBypassDnd(true)
|
|
603
633
|
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
604
|
-
// **CRITICAL FIX**: Set sound to null. We will manage the ringtone manually.
|
|
605
634
|
setSound(null, null)
|
|
606
635
|
}
|
|
607
636
|
val manager = context.getSystemService(NotificationManager::class.java)
|
|
@@ -612,7 +641,7 @@ object CallEngine {
|
|
|
612
641
|
private fun showIncomingCallUI(callId: String, callerName: String, callType: String, callerPicUrl: String?) {
|
|
613
642
|
val context = requireContext()
|
|
614
643
|
Log.d(TAG, "Showing incoming call UI for $callId")
|
|
615
|
-
createNotificationChannel()
|
|
644
|
+
createNotificationChannel()
|
|
616
645
|
|
|
617
646
|
showCallActivityOverlay(context, callId, callerName, callType, callerPicUrl)
|
|
618
647
|
showStandardNotification(context, callId, callerName, callType, callerPicUrl)
|
|
@@ -767,6 +796,9 @@ object CallEngine {
|
|
|
767
796
|
)
|
|
768
797
|
try {
|
|
769
798
|
context.startActivity(launchIntent)
|
|
799
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
800
|
+
updateLockScreenBypass()
|
|
801
|
+
}, 100)
|
|
770
802
|
} catch (e: Exception) {
|
|
771
803
|
Log.e(TAG, "Failed to bring app to foreground: ${e.message}")
|
|
772
804
|
}
|