@qusaieilouti99/call-manager 0.1.64 → 0.1.66
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 +104 -299
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/CallForegroundService.kt +12 -43
- package/lib/typescript/src/CallEventType.d.ts +1 -1
- package/lib/typescript/src/CallEventType.d.ts.map +1 -1
- package/nitrogen/generated/android/c++/JCallEventType.hpp +3 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/qusaieilouti99/callmanager/CallEventType.kt +1 -0
- package/nitrogen/generated/ios/swift/CallEventType.swift +4 -0
- package/nitrogen/generated/shared/c++/CallEventType.hpp +7 -3
- package/package.json +1 -1
- package/src/CallEventType.ts +1 -0
|
@@ -44,8 +44,6 @@ object CallEngine {
|
|
|
44
44
|
private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
|
|
45
45
|
private const val NOTIF_CHANNEL_ID = "incoming_call_channel"
|
|
46
46
|
private const val NOTIF_ID = 2001
|
|
47
|
-
private const val FOREGROUND_CHANNEL_ID = "call_foreground_channel"
|
|
48
|
-
private const val FOREGROUND_NOTIF_ID = 1001
|
|
49
47
|
|
|
50
48
|
// Core context - initialized once and maintained
|
|
51
49
|
@Volatile
|
|
@@ -53,12 +51,13 @@ object CallEngine {
|
|
|
53
51
|
private val isInitialized = AtomicBoolean(false)
|
|
54
52
|
private val initializationLock = Any()
|
|
55
53
|
|
|
56
|
-
// Audio & Media
|
|
54
|
+
// Audio & Media - SIMPLIFIED
|
|
57
55
|
private var ringtone: android.media.Ringtone? = null
|
|
58
56
|
private var ringbackPlayer: MediaPlayer? = null
|
|
59
57
|
private var audioManager: AudioManager? = null
|
|
60
58
|
private var wakeLock: PowerManager.WakeLock? = null
|
|
61
59
|
private var audioFocusRequest: AudioFocusRequest? = null
|
|
60
|
+
private var hasAudioFocus: Boolean = false
|
|
62
61
|
|
|
63
62
|
// Call State Management
|
|
64
63
|
private val activeCalls = ConcurrentHashMap<String, CallInfo>()
|
|
@@ -68,10 +67,8 @@ object CallEngine {
|
|
|
68
67
|
private var currentCallId: String? = null
|
|
69
68
|
private var canMakeMultipleCalls: Boolean = false
|
|
70
69
|
|
|
71
|
-
// Audio State Tracking
|
|
70
|
+
// Audio State Tracking - SIMPLIFIED
|
|
72
71
|
private var lastAudioRoutesInfo: AudioRoutesInfo? = null
|
|
73
|
-
private var hasAudioFocus: Boolean = false
|
|
74
|
-
private var isSystemCallActive: Boolean = false
|
|
75
72
|
|
|
76
73
|
// Lock Screen Bypass
|
|
77
74
|
private var lockScreenBypassActive = false
|
|
@@ -85,25 +82,17 @@ object CallEngine {
|
|
|
85
82
|
fun onLockScreenBypassChanged(shouldBypass: Boolean)
|
|
86
83
|
}
|
|
87
84
|
|
|
88
|
-
// --- INITIALIZATION
|
|
85
|
+
// --- INITIALIZATION ---
|
|
89
86
|
fun initialize(context: Context) {
|
|
90
87
|
synchronized(initializationLock) {
|
|
91
88
|
if (isInitialized.compareAndSet(false, true)) {
|
|
92
89
|
appContext = context.applicationContext
|
|
93
90
|
audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
94
|
-
Log.d(TAG, "CallEngine initialized successfully
|
|
95
|
-
|
|
96
|
-
// Verify critical services are available
|
|
97
|
-
if (audioManager == null) {
|
|
98
|
-
Log.w(TAG, "AudioManager is null after initialization")
|
|
99
|
-
}
|
|
91
|
+
Log.d(TAG, "CallEngine initialized successfully")
|
|
100
92
|
|
|
101
|
-
// Initialize foreground service if needed
|
|
102
93
|
if (isCallActive()) {
|
|
103
94
|
startForegroundService()
|
|
104
95
|
}
|
|
105
|
-
} else {
|
|
106
|
-
Log.d(TAG, "CallEngine already initialized, skipping")
|
|
107
96
|
}
|
|
108
97
|
}
|
|
109
98
|
}
|
|
@@ -112,7 +101,7 @@ object CallEngine {
|
|
|
112
101
|
|
|
113
102
|
private fun requireContext(): Context {
|
|
114
103
|
return appContext ?: throw IllegalStateException(
|
|
115
|
-
"CallEngine not initialized.
|
|
104
|
+
"CallEngine not initialized. Call initialize() in Application.onCreate()"
|
|
116
105
|
)
|
|
117
106
|
}
|
|
118
107
|
|
|
@@ -129,93 +118,41 @@ object CallEngine {
|
|
|
129
118
|
}
|
|
130
119
|
}
|
|
131
120
|
|
|
132
|
-
// Made public for MyConnection
|
|
133
121
|
fun emitEvent(type: CallEventType, data: JSONObject) {
|
|
134
|
-
Log.d(TAG, "Emitting event: $type
|
|
122
|
+
Log.d(TAG, "Emitting event: $type")
|
|
135
123
|
val dataString = data.toString()
|
|
136
124
|
if (eventHandler != null) {
|
|
137
125
|
eventHandler?.invoke(type, dataString)
|
|
138
126
|
} else {
|
|
139
|
-
Log.d(TAG, "No event handler
|
|
127
|
+
Log.d(TAG, "No event handler, caching event: $type")
|
|
140
128
|
cachedEvents.add(Pair(type, dataString))
|
|
141
129
|
}
|
|
142
130
|
}
|
|
143
131
|
|
|
144
|
-
// ---
|
|
132
|
+
// --- SIMPLIFIED Audio Focus Management ---
|
|
145
133
|
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
|
146
134
|
Log.d(TAG, "Audio focus changed: $focusChange")
|
|
147
135
|
when (focusChange) {
|
|
148
136
|
AudioManager.AUDIOFOCUS_LOSS -> {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
isSystemCallActive = true
|
|
152
|
-
holdSystemCalls()
|
|
153
|
-
}
|
|
154
|
-
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
155
|
-
Log.d(TAG, "Transient audio focus loss - temporary interruption")
|
|
156
|
-
hasAudioFocus = false
|
|
157
|
-
isSystemCallActive = true
|
|
158
|
-
holdSystemCalls()
|
|
159
|
-
}
|
|
160
|
-
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
|
161
|
-
Log.d(TAG, "Audio focus loss with ducking - lowering volume but keeping active")
|
|
162
|
-
// Don't hold the call for ducking, just lower volume
|
|
137
|
+
// Only hold calls on PERMANENT audio focus loss (like system calls)
|
|
138
|
+
Log.d(TAG, "Permanent audio focus loss - holding calls")
|
|
163
139
|
hasAudioFocus = false
|
|
140
|
+
holdAllActiveCalls(heldBySystem = true)
|
|
164
141
|
}
|
|
165
142
|
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
166
|
-
Log.d(TAG, "Audio focus gained")
|
|
143
|
+
Log.d(TAG, "Audio focus gained - resuming held calls")
|
|
167
144
|
hasAudioFocus = true
|
|
168
|
-
isSystemCallActive = false
|
|
169
|
-
// Delay resuming to avoid rapid hold/unhold cycles
|
|
170
145
|
Handler(Looper.getMainLooper()).postDelayed({
|
|
171
146
|
resumeSystemHeldCalls()
|
|
172
|
-
},
|
|
173
|
-
}
|
|
174
|
-
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> {
|
|
175
|
-
Log.d(TAG, "Transient audio focus gained")
|
|
176
|
-
hasAudioFocus = true
|
|
147
|
+
}, 1000)
|
|
177
148
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
val callsToHold = activeCalls.values.filter {
|
|
184
|
-
it.state == CallState.ACTIVE && !it.wasHeldBySystem
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (callsToHold.isEmpty()) {
|
|
188
|
-
Log.d(TAG, "No active calls to hold due to audio focus loss")
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
Log.d(TAG, "Holding ${callsToHold.size} calls due to audio focus loss")
|
|
193
|
-
callsToHold.forEach { call ->
|
|
194
|
-
// Add a small delay to prevent holding immediately after answering
|
|
195
|
-
val timeSinceAnswer = System.currentTimeMillis() - call.timestamp
|
|
196
|
-
if (timeSinceAnswer > 2000) { // Only hold if call has been active for 2+ seconds
|
|
197
|
-
holdCallInternal(call.callId, heldBySystem = true)
|
|
198
|
-
} else {
|
|
199
|
-
Log.d(TAG, "Skipping hold for recently answered call: ${call.callId}")
|
|
149
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT,
|
|
150
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
|
151
|
+
// Don't hold calls for transient losses - just note we lost focus
|
|
152
|
+
Log.d(TAG, "Transient audio focus loss - keeping calls active")
|
|
153
|
+
hasAudioFocus = false
|
|
200
154
|
}
|
|
201
155
|
}
|
|
202
|
-
stopRingback()
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
private fun resumeSystemHeldCalls() {
|
|
206
|
-
val callsToResume = activeCalls.values.filter {
|
|
207
|
-
it.state == CallState.HELD && it.wasHeldBySystem
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (callsToResume.isEmpty()) {
|
|
211
|
-
Log.d(TAG, "No system-held calls to resume")
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
Log.d(TAG, "Resuming ${callsToResume.size} system-held calls")
|
|
216
|
-
callsToResume.forEach { call ->
|
|
217
|
-
unholdCallInternal(call.callId, resumedBySystem = true)
|
|
218
|
-
}
|
|
219
156
|
}
|
|
220
157
|
|
|
221
158
|
private fun requestAudioFocus(): Boolean {
|
|
@@ -231,8 +168,8 @@ object CallEngine {
|
|
|
231
168
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
232
169
|
.build()
|
|
233
170
|
)
|
|
234
|
-
.setOnAudioFocusChangeListener(audioFocusChangeListener
|
|
235
|
-
.setAcceptsDelayedFocusGain(true)
|
|
171
|
+
.setOnAudioFocusChangeListener(audioFocusChangeListener)
|
|
172
|
+
.setAcceptsDelayedFocusGain(true)
|
|
236
173
|
.build()
|
|
237
174
|
}
|
|
238
175
|
val result = audioManager?.requestAudioFocus(audioFocusRequest!!)
|
|
@@ -247,7 +184,6 @@ object CallEngine {
|
|
|
247
184
|
AudioManager.AUDIOFOCUS_GAIN
|
|
248
185
|
)
|
|
249
186
|
hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
250
|
-
Log.d(TAG, "Audio focus request result (legacy): $result (granted: $hasAudioFocus)")
|
|
251
187
|
hasAudioFocus
|
|
252
188
|
}
|
|
253
189
|
}
|
|
@@ -267,6 +203,18 @@ object CallEngine {
|
|
|
267
203
|
Log.d(TAG, "Audio focus abandoned")
|
|
268
204
|
}
|
|
269
205
|
|
|
206
|
+
private fun holdAllActiveCalls(heldBySystem: Boolean) {
|
|
207
|
+
activeCalls.values.filter { it.state == CallState.ACTIVE }.forEach { call ->
|
|
208
|
+
holdCallInternal(call.callId, heldBySystem = heldBySystem)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private fun resumeSystemHeldCalls() {
|
|
213
|
+
activeCalls.values.filter { it.state == CallState.HELD && it.wasHeldBySystem }.forEach { call ->
|
|
214
|
+
unholdCallInternal(call.callId, resumedBySystem = true)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
270
218
|
// --- Lock Screen Bypass Management ---
|
|
271
219
|
fun registerLockScreenBypassCallback(callback: LockScreenBypassCallback) {
|
|
272
220
|
lockScreenBypassCallbacks.add(callback)
|
|
@@ -296,12 +244,12 @@ object CallEngine {
|
|
|
296
244
|
// --- Telecom Connection Management ---
|
|
297
245
|
fun addTelecomConnection(callId: String, connection: Connection) {
|
|
298
246
|
telecomConnections[callId] = connection
|
|
299
|
-
Log.d(TAG, "Added Telecom Connection for callId: $callId
|
|
247
|
+
Log.d(TAG, "Added Telecom Connection for callId: $callId")
|
|
300
248
|
}
|
|
301
249
|
|
|
302
250
|
fun removeTelecomConnection(callId: String) {
|
|
303
251
|
telecomConnections.remove(callId)?.let {
|
|
304
|
-
Log.d(TAG, "Removed Telecom Connection for callId: $callId
|
|
252
|
+
Log.d(TAG, "Removed Telecom Connection for callId: $callId")
|
|
305
253
|
}
|
|
306
254
|
}
|
|
307
255
|
|
|
@@ -319,9 +267,7 @@ object CallEngine {
|
|
|
319
267
|
calls.forEach {
|
|
320
268
|
jsonArray.put(it.toJsonObject())
|
|
321
269
|
}
|
|
322
|
-
|
|
323
|
-
Log.d(TAG, "Current call state: $result")
|
|
324
|
-
return result
|
|
270
|
+
return jsonArray.toString()
|
|
325
271
|
}
|
|
326
272
|
|
|
327
273
|
// --- Incoming Call Management ---
|
|
@@ -368,7 +314,7 @@ object CallEngine {
|
|
|
368
314
|
|
|
369
315
|
activeCalls[callId] = CallInfo(callId, callType, displayName, pictureUrl, CallState.INCOMING)
|
|
370
316
|
currentCallId = callId
|
|
371
|
-
Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING
|
|
317
|
+
Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING")
|
|
372
318
|
|
|
373
319
|
showIncomingCallUI(callId, displayName, callType)
|
|
374
320
|
registerPhoneAccount()
|
|
@@ -387,9 +333,6 @@ object CallEngine {
|
|
|
387
333
|
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
|
|
388
334
|
startForegroundService()
|
|
389
335
|
Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
|
|
390
|
-
} catch (e: SecurityException) {
|
|
391
|
-
Log.e(TAG, "SecurityException: Failed to report incoming call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
|
|
392
|
-
endCallInternal(callId)
|
|
393
336
|
} catch (e: Exception) {
|
|
394
337
|
Log.e(TAG, "Failed to report incoming call: ${e.message}", e)
|
|
395
338
|
endCallInternal(callId)
|
|
@@ -435,7 +378,6 @@ object CallEngine {
|
|
|
435
378
|
val phoneAccountHandle = getPhoneAccountHandle()
|
|
436
379
|
val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
|
|
437
380
|
|
|
438
|
-
// Build a bundle of ONLY your own keys
|
|
439
381
|
val outgoingExtras = Bundle().apply {
|
|
440
382
|
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
441
383
|
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
@@ -444,7 +386,6 @@ object CallEngine {
|
|
|
444
386
|
metadata?.let { putString("metadata", it) }
|
|
445
387
|
}
|
|
446
388
|
|
|
447
|
-
// Wrap under the single Telecom-honored key
|
|
448
389
|
val extras = Bundle().apply {
|
|
449
390
|
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
|
|
450
391
|
putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
|
|
@@ -455,21 +396,14 @@ object CallEngine {
|
|
|
455
396
|
telecomManager.placeCall(addressUri, extras)
|
|
456
397
|
startForegroundService()
|
|
457
398
|
|
|
458
|
-
//
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
startRingback()
|
|
462
|
-
} else {
|
|
463
|
-
Log.w(TAG, "Audio focus not granted for outgoing call, skipping ringback")
|
|
464
|
-
}
|
|
399
|
+
// SIMPLIFIED: Request audio focus and start ringback
|
|
400
|
+
requestAudioFocus()
|
|
401
|
+
startRingback()
|
|
465
402
|
|
|
466
403
|
bringAppToForeground()
|
|
467
404
|
keepScreenAwake(true)
|
|
468
405
|
setInitialAudioRoute(callType)
|
|
469
406
|
Log.d(TAG, "Successfully reported outgoing call to TelecomManager")
|
|
470
|
-
} catch (e: SecurityException) {
|
|
471
|
-
Log.e(TAG, "SecurityException placing outgoing call: ${e.message}", e)
|
|
472
|
-
endCallInternal(callId)
|
|
473
407
|
} catch (e: Exception) {
|
|
474
408
|
Log.e(TAG, "Failed to start outgoing call: ${e.message}", e)
|
|
475
409
|
endCallInternal(callId)
|
|
@@ -478,14 +412,12 @@ object CallEngine {
|
|
|
478
412
|
updateLockScreenBypass()
|
|
479
413
|
}
|
|
480
414
|
|
|
481
|
-
// Fixed: Start call as active (not dialing) with foreground service
|
|
482
415
|
fun startCall(
|
|
483
416
|
callId: String,
|
|
484
417
|
callType: String,
|
|
485
418
|
targetName: String,
|
|
486
419
|
metadata: String? = null
|
|
487
420
|
) {
|
|
488
|
-
val context = requireContext()
|
|
489
421
|
Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
|
|
490
422
|
|
|
491
423
|
metadata?.let { callMetadata[callId] = it }
|
|
@@ -506,7 +438,7 @@ object CallEngine {
|
|
|
506
438
|
// Start directly as active call
|
|
507
439
|
activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
|
|
508
440
|
currentCallId = callId
|
|
509
|
-
Log.d(TAG, "Call $callId started as ACTIVE
|
|
441
|
+
Log.d(TAG, "Call $callId started as ACTIVE")
|
|
510
442
|
|
|
511
443
|
registerPhoneAccount()
|
|
512
444
|
requestAudioFocus()
|
|
@@ -516,11 +448,11 @@ object CallEngine {
|
|
|
516
448
|
setInitialAudioRoute(callType)
|
|
517
449
|
updateLockScreenBypass()
|
|
518
450
|
|
|
519
|
-
// Emit outgoing call answered event
|
|
451
|
+
// Emit outgoing call answered event
|
|
520
452
|
emitOutgoingCallAnsweredWithMetadata(callId)
|
|
521
453
|
}
|
|
522
454
|
|
|
523
|
-
// --- Call Answer Management ---
|
|
455
|
+
// --- Call Answer Management - SIMPLIFIED ---
|
|
524
456
|
fun callAnsweredFromJS(callId: String) {
|
|
525
457
|
Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
|
|
526
458
|
coreCallAnswered(callId, isLocalAnswer = false)
|
|
@@ -531,9 +463,8 @@ object CallEngine {
|
|
|
531
463
|
coreCallAnswered(callId, isLocalAnswer = true)
|
|
532
464
|
}
|
|
533
465
|
|
|
534
|
-
//
|
|
466
|
+
// SIMPLIFIED: Always succeed call answer, handle audio focus gracefully
|
|
535
467
|
private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
|
|
536
|
-
val context = requireContext()
|
|
537
468
|
Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
|
|
538
469
|
|
|
539
470
|
val callInfo = activeCalls[callId]
|
|
@@ -542,27 +473,18 @@ object CallEngine {
|
|
|
542
473
|
return
|
|
543
474
|
}
|
|
544
475
|
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
// Don't fail the call, but warn about audio issues
|
|
550
|
-
}
|
|
476
|
+
// ALWAYS set call to ACTIVE - don't fail due to audio focus
|
|
477
|
+
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
478
|
+
currentCallId = callId
|
|
479
|
+
Log.d(TAG, "Call $callId set to ACTIVE state")
|
|
551
480
|
|
|
552
|
-
//
|
|
481
|
+
// Clean up media and UI
|
|
553
482
|
stopRingtone()
|
|
554
483
|
stopRingback()
|
|
555
484
|
cancelIncomingCallUI()
|
|
556
485
|
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
560
|
-
currentCallId = callId
|
|
561
|
-
Log.d(TAG, "Call $callId set to ACTIVE state")
|
|
562
|
-
} else {
|
|
563
|
-
Log.w(TAG, "Call $callId not set to ACTIVE due to audio focus failure")
|
|
564
|
-
return
|
|
565
|
-
}
|
|
486
|
+
// Request audio focus (but don't fail if not granted)
|
|
487
|
+
requestAudioFocus()
|
|
566
488
|
|
|
567
489
|
if (!canMakeMultipleCalls) {
|
|
568
490
|
activeCalls.filter { it.key != callId }.values.forEach { otherCall ->
|
|
@@ -575,20 +497,17 @@ object CallEngine {
|
|
|
575
497
|
bringAppToForeground()
|
|
576
498
|
startForegroundService()
|
|
577
499
|
keepScreenAwake(true)
|
|
578
|
-
|
|
500
|
+
setAudioMode()
|
|
579
501
|
updateLockScreenBypass()
|
|
580
|
-
updateForegroundNotification()
|
|
581
502
|
|
|
582
|
-
//
|
|
503
|
+
// Always emit events based on call direction
|
|
583
504
|
if (isLocalAnswer) {
|
|
584
|
-
// This is for incoming calls - user answered locally
|
|
585
505
|
emitCallAnsweredWithMetadata(callId)
|
|
586
506
|
} else {
|
|
587
|
-
// This is for outgoing calls - remote party answered
|
|
588
507
|
emitOutgoingCallAnsweredWithMetadata(callId)
|
|
589
508
|
}
|
|
590
509
|
|
|
591
|
-
Log.d(TAG, "Call $callId successfully answered
|
|
510
|
+
Log.d(TAG, "Call $callId successfully answered")
|
|
592
511
|
}
|
|
593
512
|
|
|
594
513
|
// For incoming calls (local answer)
|
|
@@ -605,8 +524,7 @@ object CallEngine {
|
|
|
605
524
|
try {
|
|
606
525
|
put("metadata", JSONObject(it))
|
|
607
526
|
} catch (e: Exception) {
|
|
608
|
-
|
|
609
|
-
put("metadata", it) // fallback to string
|
|
527
|
+
put("metadata", it)
|
|
610
528
|
}
|
|
611
529
|
}
|
|
612
530
|
})
|
|
@@ -626,8 +544,7 @@ object CallEngine {
|
|
|
626
544
|
try {
|
|
627
545
|
put("metadata", JSONObject(it))
|
|
628
546
|
} catch (e: Exception) {
|
|
629
|
-
|
|
630
|
-
put("metadata", it) // fallback to string
|
|
547
|
+
put("metadata", it)
|
|
631
548
|
}
|
|
632
549
|
}
|
|
633
550
|
})
|
|
@@ -640,21 +557,16 @@ object CallEngine {
|
|
|
640
557
|
|
|
641
558
|
fun setOnHold(callId: String, onHold: Boolean) {
|
|
642
559
|
Log.d(TAG, "setOnHold: $callId, onHold: $onHold")
|
|
643
|
-
|
|
644
560
|
val callInfo = activeCalls[callId]
|
|
645
561
|
if (callInfo == null) {
|
|
646
|
-
Log.w(TAG, "Cannot set hold state for call $callId - not found
|
|
562
|
+
Log.w(TAG, "Cannot set hold state for call $callId - not found")
|
|
647
563
|
return
|
|
648
564
|
}
|
|
649
565
|
|
|
650
|
-
if (onHold) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
} else {
|
|
655
|
-
if (callInfo.state == CallState.HELD) {
|
|
656
|
-
unholdCallInternal(callId, resumedBySystem = false)
|
|
657
|
-
}
|
|
566
|
+
if (onHold && callInfo.state == CallState.ACTIVE) {
|
|
567
|
+
holdCallInternal(callId, heldBySystem = false)
|
|
568
|
+
} else if (!onHold && callInfo.state == CallState.HELD) {
|
|
569
|
+
unholdCallInternal(callId, resumedBySystem = false)
|
|
658
570
|
}
|
|
659
571
|
}
|
|
660
572
|
|
|
@@ -662,7 +574,7 @@ object CallEngine {
|
|
|
662
574
|
Log.d(TAG, "holdCallInternal: $callId, heldBySystem: $heldBySystem")
|
|
663
575
|
val callInfo = activeCalls[callId]
|
|
664
576
|
if (callInfo?.state != CallState.ACTIVE) {
|
|
665
|
-
Log.w(TAG, "Cannot hold call $callId - not in active state
|
|
577
|
+
Log.w(TAG, "Cannot hold call $callId - not in active state")
|
|
666
578
|
return
|
|
667
579
|
}
|
|
668
580
|
|
|
@@ -671,9 +583,7 @@ object CallEngine {
|
|
|
671
583
|
wasHeldBySystem = heldBySystem
|
|
672
584
|
)
|
|
673
585
|
|
|
674
|
-
|
|
675
|
-
connection?.setOnHold()
|
|
676
|
-
|
|
586
|
+
telecomConnections[callId]?.setOnHold()
|
|
677
587
|
updateForegroundNotification()
|
|
678
588
|
emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
|
|
679
589
|
updateLockScreenBypass()
|
|
@@ -687,29 +597,19 @@ object CallEngine {
|
|
|
687
597
|
Log.d(TAG, "unholdCallInternal: $callId, resumedBySystem: $resumedBySystem")
|
|
688
598
|
val callInfo = activeCalls[callId]
|
|
689
599
|
if (callInfo?.state != CallState.HELD) {
|
|
690
|
-
Log.w(TAG, "Cannot unhold call $callId - not in held state
|
|
600
|
+
Log.w(TAG, "Cannot unhold call $callId - not in held state")
|
|
691
601
|
return
|
|
692
602
|
}
|
|
693
603
|
|
|
694
|
-
// FIXED: Simplified audio focus check to prevent UNHELD FAILED
|
|
695
|
-
if (!hasAudioFocus && !resumedBySystem && !requestAudioFocus()) {
|
|
696
|
-
Log.w(TAG, "Failed to get audio focus for unhold - but continuing anyway")
|
|
697
|
-
// Don't emit UNHELD FAILED - just continue
|
|
698
|
-
}
|
|
699
|
-
|
|
700
604
|
activeCalls[callId] = callInfo.copy(
|
|
701
605
|
state = CallState.ACTIVE,
|
|
702
606
|
wasHeldBySystem = false
|
|
703
607
|
)
|
|
704
608
|
|
|
705
|
-
|
|
706
|
-
connection?.setActive()
|
|
707
|
-
|
|
609
|
+
telecomConnections[callId]?.setActive()
|
|
708
610
|
updateForegroundNotification()
|
|
709
611
|
emitEvent(CallEventType.CALL_UNHELD, JSONObject().put("callId", callId))
|
|
710
612
|
updateLockScreenBypass()
|
|
711
|
-
|
|
712
|
-
Log.d(TAG, "Call $callId successfully unheld")
|
|
713
613
|
}
|
|
714
614
|
|
|
715
615
|
fun muteCall(callId: String) {
|
|
@@ -721,18 +621,17 @@ object CallEngine {
|
|
|
721
621
|
}
|
|
722
622
|
|
|
723
623
|
fun setMuted(callId: String, muted: Boolean) {
|
|
724
|
-
Log.d(TAG, "setMuted: $callId, muted: $muted")
|
|
725
624
|
setMutedInternal(callId, muted)
|
|
726
625
|
}
|
|
727
626
|
|
|
728
627
|
private fun setMutedInternal(callId: String, muted: Boolean) {
|
|
729
|
-
val context = requireContext()
|
|
730
628
|
val callInfo = activeCalls[callId]
|
|
731
629
|
if (callInfo == null) {
|
|
732
|
-
Log.w(TAG, "Cannot set mute state for call $callId - not found
|
|
630
|
+
Log.w(TAG, "Cannot set mute state for call $callId - not found")
|
|
733
631
|
return
|
|
734
632
|
}
|
|
735
633
|
|
|
634
|
+
val context = requireContext()
|
|
736
635
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
737
636
|
|
|
738
637
|
val wasMuted = audioManager?.isMicrophoneMute ?: false
|
|
@@ -752,11 +651,8 @@ object CallEngine {
|
|
|
752
651
|
}
|
|
753
652
|
|
|
754
653
|
fun endAllCalls() {
|
|
755
|
-
Log.d(TAG, "endAllCalls: Ending all active calls
|
|
756
|
-
if (activeCalls.isEmpty())
|
|
757
|
-
Log.d(TAG, "No active calls, nothing to do.")
|
|
758
|
-
return
|
|
759
|
-
}
|
|
654
|
+
Log.d(TAG, "endAllCalls: Ending all active calls")
|
|
655
|
+
if (activeCalls.isEmpty()) return
|
|
760
656
|
|
|
761
657
|
activeCalls.keys.toList().forEach { callId ->
|
|
762
658
|
endCallInternal(callId)
|
|
@@ -767,7 +663,7 @@ object CallEngine {
|
|
|
767
663
|
callMetadata.clear()
|
|
768
664
|
currentCallId = null
|
|
769
665
|
|
|
770
|
-
|
|
666
|
+
cleanup()
|
|
771
667
|
updateLockScreenBypass()
|
|
772
668
|
}
|
|
773
669
|
|
|
@@ -779,12 +675,10 @@ object CallEngine {
|
|
|
779
675
|
return
|
|
780
676
|
}
|
|
781
677
|
|
|
782
|
-
// Get metadata before removing
|
|
783
678
|
val metadata = callMetadata.remove(callId)
|
|
784
679
|
|
|
785
680
|
activeCalls[callId] = callInfo.copy(state = CallState.ENDED)
|
|
786
681
|
activeCalls.remove(callId)
|
|
787
|
-
Log.d(TAG, "Call $callId removed from activeCalls. Remaining: ${activeCalls.size}")
|
|
788
682
|
|
|
789
683
|
stopRingback()
|
|
790
684
|
stopRingtone()
|
|
@@ -792,7 +686,6 @@ object CallEngine {
|
|
|
792
686
|
|
|
793
687
|
if (currentCallId == callId) {
|
|
794
688
|
currentCallId = activeCalls.filter { it.value.state != CallState.ENDED }.keys.firstOrNull()
|
|
795
|
-
Log.d(TAG, "Current call was $callId. New currentCallId: $currentCallId")
|
|
796
689
|
}
|
|
797
690
|
|
|
798
691
|
val connection = telecomConnections[callId]
|
|
@@ -800,11 +693,10 @@ object CallEngine {
|
|
|
800
693
|
connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
801
694
|
connection.destroy()
|
|
802
695
|
removeTelecomConnection(callId)
|
|
803
|
-
Log.d(TAG, "Telecom Connection for $callId disconnected and destroyed.")
|
|
804
696
|
}
|
|
805
697
|
|
|
806
698
|
if (activeCalls.isEmpty()) {
|
|
807
|
-
|
|
699
|
+
cleanup()
|
|
808
700
|
} else {
|
|
809
701
|
updateForegroundNotification()
|
|
810
702
|
}
|
|
@@ -818,8 +710,7 @@ object CallEngine {
|
|
|
818
710
|
try {
|
|
819
711
|
put("metadata", JSONObject(it))
|
|
820
712
|
} catch (e: Exception) {
|
|
821
|
-
|
|
822
|
-
put("metadata", it) // fallback to string
|
|
713
|
+
put("metadata", it)
|
|
823
714
|
}
|
|
824
715
|
}
|
|
825
716
|
})
|
|
@@ -829,7 +720,6 @@ object CallEngine {
|
|
|
829
720
|
fun getAudioDevices(): AudioRoutesInfo {
|
|
830
721
|
val context = requireContext()
|
|
831
722
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
|
|
832
|
-
Log.e(TAG, "getAudioDevices: AudioManager is null. Returning default.")
|
|
833
723
|
return AudioRoutesInfo(emptyArray(), "Unknown")
|
|
834
724
|
}
|
|
835
725
|
|
|
@@ -865,15 +755,13 @@ object CallEngine {
|
|
|
865
755
|
else -> "Earpiece"
|
|
866
756
|
}
|
|
867
757
|
|
|
868
|
-
|
|
869
|
-
Log.d(TAG, "Audio devices info: $result")
|
|
870
|
-
return result
|
|
758
|
+
return AudioRoutesInfo(devices.toTypedArray(), currentRoute)
|
|
871
759
|
}
|
|
872
760
|
|
|
873
761
|
fun setAudioRoute(route: String) {
|
|
874
762
|
val context = requireContext()
|
|
875
763
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
876
|
-
Log.d(TAG, "
|
|
764
|
+
Log.d(TAG, "Setting audio route to: $route")
|
|
877
765
|
|
|
878
766
|
val previousRoute = getCurrentAudioRoute()
|
|
879
767
|
|
|
@@ -883,26 +771,22 @@ object CallEngine {
|
|
|
883
771
|
|
|
884
772
|
when (route) {
|
|
885
773
|
"Speaker" -> {
|
|
886
|
-
Log.d(TAG, "Setting audio route to Speaker.")
|
|
887
774
|
audioManager?.isSpeakerphoneOn = true
|
|
888
775
|
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
889
776
|
}
|
|
890
777
|
"Earpiece" -> {
|
|
891
|
-
Log.d(TAG, "Setting audio route to Earpiece.")
|
|
892
778
|
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
893
779
|
}
|
|
894
780
|
"Bluetooth" -> {
|
|
895
|
-
Log.d(TAG, "Setting audio route to Bluetooth.")
|
|
896
781
|
audioManager?.startBluetoothSco()
|
|
897
782
|
audioManager?.isBluetoothScoOn = true
|
|
898
783
|
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
899
784
|
}
|
|
900
785
|
"Headset" -> {
|
|
901
|
-
Log.d(TAG, "Setting audio route to Headset (wired).")
|
|
902
786
|
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
903
787
|
}
|
|
904
788
|
else -> {
|
|
905
|
-
Log.w(TAG, "Unknown audio route: $route
|
|
789
|
+
Log.w(TAG, "Unknown audio route: $route")
|
|
906
790
|
return
|
|
907
791
|
}
|
|
908
792
|
}
|
|
@@ -932,34 +816,31 @@ object CallEngine {
|
|
|
932
816
|
else -> "Earpiece"
|
|
933
817
|
}
|
|
934
818
|
|
|
935
|
-
Log.d(TAG, "Setting initial audio route
|
|
819
|
+
Log.d(TAG, "Setting initial audio route: $defaultRoute")
|
|
936
820
|
setAudioRoute(defaultRoute)
|
|
937
821
|
}
|
|
938
822
|
|
|
823
|
+
private fun setAudioMode() {
|
|
824
|
+
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
825
|
+
}
|
|
826
|
+
|
|
939
827
|
private fun resetAudioMode() {
|
|
940
|
-
val context = requireContext()
|
|
941
|
-
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
942
828
|
if (activeCalls.isEmpty()) {
|
|
943
|
-
Log.d(TAG, "Resetting audio mode to NORMAL as no active calls remain.")
|
|
944
829
|
audioManager?.mode = AudioManager.MODE_NORMAL
|
|
945
830
|
audioManager?.stopBluetoothSco()
|
|
946
831
|
audioManager?.isBluetoothScoOn = false
|
|
947
832
|
audioManager?.isSpeakerphoneOn = false
|
|
948
833
|
abandonAudioFocus()
|
|
949
|
-
} else {
|
|
950
|
-
Log.d(TAG, "Audio mode not reset; ${activeCalls.size} calls still active.")
|
|
951
834
|
}
|
|
952
835
|
}
|
|
953
836
|
|
|
954
837
|
// --- Audio Device Callback ---
|
|
955
838
|
private val audioDeviceCallback = object : AudioDeviceCallback() {
|
|
956
839
|
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
|
|
957
|
-
Log.d(TAG, "Audio devices added. Checking for changes.")
|
|
958
840
|
emitAudioDevicesChangedIfNeeded()
|
|
959
841
|
}
|
|
960
842
|
|
|
961
843
|
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
|
|
962
|
-
Log.d(TAG, "Audio devices removed. Checking for changes.")
|
|
963
844
|
emitAudioDevicesChangedIfNeeded()
|
|
964
845
|
}
|
|
965
846
|
}
|
|
@@ -968,14 +849,12 @@ object CallEngine {
|
|
|
968
849
|
val context = requireContext()
|
|
969
850
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
970
851
|
audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
|
|
971
|
-
Log.d(TAG, "Audio device callback registered.")
|
|
972
852
|
}
|
|
973
853
|
|
|
974
854
|
fun unregisterAudioDeviceCallback() {
|
|
975
855
|
val context = requireContext()
|
|
976
856
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
977
857
|
audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
|
|
978
|
-
Log.d(TAG, "Audio device callback unregistered.")
|
|
979
858
|
}
|
|
980
859
|
|
|
981
860
|
private fun emitAudioDevicesChangedIfNeeded() {
|
|
@@ -1004,14 +883,14 @@ object CallEngine {
|
|
|
1004
883
|
PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
1005
884
|
"CallEngine:WakeLock"
|
|
1006
885
|
)
|
|
1007
|
-
wakeLock?.acquire(10 * 60 * 1000L
|
|
1008
|
-
Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK
|
|
886
|
+
wakeLock?.acquire(10 * 60 * 1000L)
|
|
887
|
+
Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK")
|
|
1009
888
|
}
|
|
1010
889
|
} else {
|
|
1011
890
|
wakeLock?.let {
|
|
1012
891
|
if (it.isHeld) {
|
|
1013
892
|
it.release()
|
|
1014
|
-
Log.d(TAG, "Released SCREEN_DIM_WAKE_LOCK
|
|
893
|
+
Log.d(TAG, "Released SCREEN_DIM_WAKE_LOCK")
|
|
1015
894
|
}
|
|
1016
895
|
}
|
|
1017
896
|
wakeLock = null
|
|
@@ -1035,17 +914,7 @@ object CallEngine {
|
|
|
1035
914
|
}
|
|
1036
915
|
|
|
1037
916
|
private fun rejectIncomingCallCollision(callId: String, reason: String) {
|
|
1038
|
-
// Remove metadata for rejected call
|
|
1039
917
|
callMetadata.remove(callId)
|
|
1040
|
-
|
|
1041
|
-
CoroutineScope(Dispatchers.IO).launch {
|
|
1042
|
-
try {
|
|
1043
|
-
Log.d(TAG, "Server rejection request would be made here for callId: $callId, reason: $reason")
|
|
1044
|
-
} catch (e: Exception) {
|
|
1045
|
-
Log.e(TAG, "Failed to send rejection to server", e)
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
918
|
emitEvent(CallEventType.CALL_REJECTED, JSONObject().apply {
|
|
1050
919
|
put("callId", callId)
|
|
1051
920
|
put("reason", reason)
|
|
@@ -1081,13 +950,12 @@ object CallEngine {
|
|
|
1081
950
|
|
|
1082
951
|
val manager = context.getSystemService(NotificationManager::class.java)
|
|
1083
952
|
manager.createNotificationChannel(channel)
|
|
1084
|
-
Log.d(TAG, "Notification channel '$NOTIF_CHANNEL_ID' created/updated.")
|
|
1085
953
|
}
|
|
1086
954
|
}
|
|
1087
955
|
|
|
1088
956
|
private fun showIncomingCallUI(callId: String, callerName: String, callType: String) {
|
|
1089
957
|
val context = requireContext()
|
|
1090
|
-
Log.d(TAG, "Showing incoming call UI for $callId
|
|
958
|
+
Log.d(TAG, "Showing incoming call UI for $callId")
|
|
1091
959
|
createNotificationChannel()
|
|
1092
960
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
1093
961
|
|
|
@@ -1153,33 +1021,27 @@ object CallEngine {
|
|
|
1153
1021
|
setInitialAudioRoute(callType)
|
|
1154
1022
|
}
|
|
1155
1023
|
|
|
1156
|
-
// Made public for CallActivity
|
|
1157
1024
|
fun cancelIncomingCallUI() {
|
|
1158
1025
|
val context = requireContext()
|
|
1159
|
-
Log.d(TAG, "Cancelling incoming call UI.")
|
|
1160
1026
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
1161
1027
|
notificationManager.cancel(NOTIF_ID)
|
|
1162
1028
|
stopRingtone()
|
|
1163
1029
|
}
|
|
1164
1030
|
|
|
1165
|
-
// --- Service Management ---
|
|
1031
|
+
// --- Service Management - SIMPLIFIED ---
|
|
1166
1032
|
private fun startForegroundService() {
|
|
1167
1033
|
val context = requireContext()
|
|
1168
|
-
Log.d(TAG, "Starting CallForegroundService.")
|
|
1169
|
-
|
|
1170
1034
|
val currentCall = activeCalls.values.find {
|
|
1171
1035
|
it.state == CallState.ACTIVE || it.state == CallState.INCOMING ||
|
|
1172
1036
|
it.state == CallState.DIALING || it.state == CallState.HELD
|
|
1173
1037
|
}
|
|
1174
1038
|
|
|
1175
1039
|
val intent = Intent(context, CallForegroundService::class.java)
|
|
1176
|
-
|
|
1177
1040
|
if (currentCall != null) {
|
|
1178
1041
|
intent.putExtra("callId", currentCall.callId)
|
|
1179
1042
|
intent.putExtra("callType", currentCall.callType)
|
|
1180
1043
|
intent.putExtra("displayName", currentCall.displayName)
|
|
1181
1044
|
intent.putExtra("state", currentCall.state.name)
|
|
1182
|
-
Log.d(TAG, "Starting foreground service with call info: ${currentCall.callId}")
|
|
1183
1045
|
}
|
|
1184
1046
|
|
|
1185
1047
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
@@ -1191,11 +1053,14 @@ object CallEngine {
|
|
|
1191
1053
|
|
|
1192
1054
|
private fun stopForegroundService() {
|
|
1193
1055
|
val context = requireContext()
|
|
1194
|
-
Log.d(TAG, "Stopping CallForegroundService.")
|
|
1195
1056
|
val intent = Intent(context, CallForegroundService::class.java)
|
|
1196
1057
|
context.stopService(intent)
|
|
1197
1058
|
}
|
|
1198
1059
|
|
|
1060
|
+
private fun updateForegroundNotification() {
|
|
1061
|
+
startForegroundService() // Just restart the service with updated info
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1199
1064
|
private fun bringAppToForeground() {
|
|
1200
1065
|
val context = requireContext()
|
|
1201
1066
|
val packageName = context.packageName
|
|
@@ -1204,11 +1069,6 @@ object CallEngine {
|
|
|
1204
1069
|
|
|
1205
1070
|
if (isCallActive()) {
|
|
1206
1071
|
launchIntent?.putExtra("BYPASS_LOCK_SCREEN", true)
|
|
1207
|
-
launchIntent?.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
|
1208
|
-
Log.d(TAG, "App brought to foreground with lock screen bypass request for active call")
|
|
1209
|
-
} else {
|
|
1210
|
-
launchIntent?.removeExtra("BYPASS_LOCK_SCREEN")
|
|
1211
|
-
Log.d(TAG, "App brought to foreground without lock screen bypass")
|
|
1212
1072
|
}
|
|
1213
1073
|
|
|
1214
1074
|
try {
|
|
@@ -1234,14 +1094,10 @@ object CallEngine {
|
|
|
1234
1094
|
|
|
1235
1095
|
try {
|
|
1236
1096
|
telecomManager.registerPhoneAccount(phoneAccount)
|
|
1237
|
-
Log.d(TAG, "PhoneAccount registered successfully
|
|
1238
|
-
} catch (e: SecurityException) {
|
|
1239
|
-
Log.e(TAG, "SecurityException: Cannot register PhoneAccount. Missing MANAGE_OWN_CALLS permission?", e)
|
|
1097
|
+
Log.d(TAG, "PhoneAccount registered successfully")
|
|
1240
1098
|
} catch (e: Exception) {
|
|
1241
1099
|
Log.e(TAG, "Failed to register PhoneAccount: ${e.message}", e)
|
|
1242
1100
|
}
|
|
1243
|
-
} else {
|
|
1244
|
-
Log.d(TAG, "PhoneAccount already registered.")
|
|
1245
1101
|
}
|
|
1246
1102
|
}
|
|
1247
1103
|
|
|
@@ -1257,115 +1113,65 @@ object CallEngine {
|
|
|
1257
1113
|
private fun playRingtone() {
|
|
1258
1114
|
val context = requireContext()
|
|
1259
1115
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
1260
|
-
|
|
1261
|
-
return
|
|
1116
|
+
return // System handles it
|
|
1262
1117
|
}
|
|
1263
1118
|
|
|
1264
1119
|
try {
|
|
1265
|
-
Log.d(TAG, "Playing ringtone (for Android < S).")
|
|
1266
1120
|
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
1267
1121
|
ringtone = RingtoneManager.getRingtone(context, uri)
|
|
1268
|
-
ringtone?.audioAttributes = AudioAttributes.Builder()
|
|
1269
|
-
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
1270
|
-
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
1271
|
-
.build()
|
|
1272
1122
|
ringtone?.play()
|
|
1273
1123
|
} catch (e: Exception) {
|
|
1274
|
-
Log.e(TAG, "Failed to play ringtone: ${e.message}"
|
|
1124
|
+
Log.e(TAG, "Failed to play ringtone: ${e.message}")
|
|
1275
1125
|
}
|
|
1276
1126
|
}
|
|
1277
1127
|
|
|
1278
|
-
// Made public for CallActivity and CallManager
|
|
1279
1128
|
fun stopRingtone() {
|
|
1280
1129
|
try {
|
|
1281
|
-
|
|
1282
|
-
ringtone?.stop()
|
|
1283
|
-
Log.d(TAG, "Ringtone stopped.")
|
|
1284
|
-
}
|
|
1130
|
+
ringtone?.stop()
|
|
1285
1131
|
} catch (e: Exception) {
|
|
1286
|
-
Log.e(TAG, "Error stopping ringtone: ${e.message}"
|
|
1132
|
+
Log.e(TAG, "Error stopping ringtone: ${e.message}")
|
|
1287
1133
|
}
|
|
1288
1134
|
ringtone = null
|
|
1289
1135
|
}
|
|
1290
1136
|
|
|
1291
1137
|
private fun startRingback() {
|
|
1292
1138
|
val context = requireContext()
|
|
1293
|
-
if (ringbackPlayer?.isPlaying == true)
|
|
1294
|
-
Log.d(TAG, "Ringback tone already playing.")
|
|
1295
|
-
return
|
|
1296
|
-
}
|
|
1139
|
+
if (ringbackPlayer?.isPlaying == true) return
|
|
1297
1140
|
|
|
1298
1141
|
try {
|
|
1299
1142
|
val ringbackUri = Uri.parse("android.resource://${context.packageName}/raw/ringback_tone")
|
|
1300
1143
|
ringbackPlayer = MediaPlayer.create(context, ringbackUri)
|
|
1301
|
-
if (ringbackPlayer == null) {
|
|
1302
|
-
Log.e(TAG, "Failed to create MediaPlayer for ringback. Check raw/ringback_tone.mp3 exists.")
|
|
1303
|
-
return
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
1144
|
ringbackPlayer?.apply {
|
|
1307
1145
|
isLooping = true
|
|
1308
|
-
setAudioAttributes(
|
|
1309
|
-
AudioAttributes.Builder()
|
|
1310
|
-
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
|
|
1311
|
-
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
1312
|
-
.build()
|
|
1313
|
-
)
|
|
1314
1146
|
start()
|
|
1315
|
-
Log.d(TAG, "Ringback tone started.")
|
|
1316
1147
|
}
|
|
1317
1148
|
} catch (e: Exception) {
|
|
1318
|
-
Log.e(TAG, "Failed to play ringback tone: ${e.message}"
|
|
1149
|
+
Log.e(TAG, "Failed to play ringback tone: ${e.message}")
|
|
1319
1150
|
}
|
|
1320
1151
|
}
|
|
1321
1152
|
|
|
1322
1153
|
private fun stopRingback() {
|
|
1323
1154
|
try {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
ringbackPlayer?.release()
|
|
1327
|
-
Log.d(TAG, "Ringback tone stopped and released.")
|
|
1328
|
-
}
|
|
1155
|
+
ringbackPlayer?.stop()
|
|
1156
|
+
ringbackPlayer?.release()
|
|
1329
1157
|
} catch (e: Exception) {
|
|
1330
|
-
Log.e(TAG, "Error stopping ringback
|
|
1158
|
+
Log.e(TAG, "Error stopping ringback: ${e.message}")
|
|
1331
1159
|
} finally {
|
|
1332
1160
|
ringbackPlayer = null
|
|
1333
1161
|
}
|
|
1334
1162
|
}
|
|
1335
1163
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
val heldCall = activeCalls.values.find { it.state == CallState.HELD }
|
|
1340
|
-
|
|
1341
|
-
val callToShow = activeCall ?: heldCall
|
|
1342
|
-
callToShow?.let {
|
|
1343
|
-
val intent = Intent(context, CallForegroundService::class.java)
|
|
1344
|
-
intent.putExtra("UPDATE_NOTIFICATION", true)
|
|
1345
|
-
intent.putExtra("callId", it.callId)
|
|
1346
|
-
intent.putExtra("callType", it.callType)
|
|
1347
|
-
intent.putExtra("displayName", it.displayName)
|
|
1348
|
-
intent.putExtra("state", it.state.name)
|
|
1349
|
-
|
|
1350
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1351
|
-
context.startForegroundService(intent)
|
|
1352
|
-
} else {
|
|
1353
|
-
context.startService(intent)
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
private fun finalCleanup() {
|
|
1359
|
-
Log.d(TAG, "Performing final cleanup - no active calls remaining")
|
|
1164
|
+
// --- Cleanup - SIMPLIFIED ---
|
|
1165
|
+
private fun cleanup() {
|
|
1166
|
+
Log.d(TAG, "Performing cleanup")
|
|
1360
1167
|
stopForegroundService()
|
|
1361
1168
|
keepScreenAwake(false)
|
|
1362
1169
|
resetAudioMode()
|
|
1363
|
-
isSystemCallActive = false
|
|
1364
1170
|
}
|
|
1365
1171
|
|
|
1366
1172
|
// --- Lifecycle Management ---
|
|
1367
1173
|
fun onApplicationTerminate() {
|
|
1368
|
-
Log.d(TAG, "Application terminating
|
|
1174
|
+
Log.d(TAG, "Application terminating")
|
|
1369
1175
|
|
|
1370
1176
|
// End all calls properly
|
|
1371
1177
|
activeCalls.keys.toList().forEach { callId ->
|
|
@@ -1380,8 +1186,7 @@ object CallEngine {
|
|
|
1380
1186
|
callMetadata.clear()
|
|
1381
1187
|
currentCallId = null
|
|
1382
1188
|
|
|
1383
|
-
|
|
1384
|
-
finalCleanup()
|
|
1189
|
+
cleanup()
|
|
1385
1190
|
|
|
1386
1191
|
// Clear callbacks
|
|
1387
1192
|
lockScreenBypassCallbacks.clear()
|
package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/CallForegroundService.kt
CHANGED
|
@@ -35,10 +35,8 @@ class CallForegroundService : Service() {
|
|
|
35
35
|
val state = intent?.getStringExtra("state")
|
|
36
36
|
|
|
37
37
|
val notification = if (callId != null && callType != null && displayName != null && state != null) {
|
|
38
|
-
Log.d(TAG, "Building enhanced notification with call info: $callId")
|
|
39
38
|
buildEnhancedNotification(callId, callType, displayName, state)
|
|
40
39
|
} else {
|
|
41
|
-
Log.d(TAG, "Building basic notification - no call info available")
|
|
42
40
|
buildBasicNotification()
|
|
43
41
|
}
|
|
44
42
|
|
|
@@ -46,14 +44,9 @@ class CallForegroundService : Service() {
|
|
|
46
44
|
return START_STICKY
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
override fun onBind(intent: Intent?): IBinder?
|
|
50
|
-
Log.d(TAG, "Service onBind")
|
|
51
|
-
return null
|
|
52
|
-
}
|
|
47
|
+
override fun onBind(intent: Intent?): IBinder? = null
|
|
53
48
|
|
|
54
49
|
private fun buildBasicNotification(): Notification {
|
|
55
|
-
Log.d(TAG, "Building basic foreground notification.")
|
|
56
|
-
|
|
57
50
|
return NotificationCompat.Builder(this, CHANNEL_ID)
|
|
58
51
|
.setContentTitle("Call Service")
|
|
59
52
|
.setContentText("Call service is running...")
|
|
@@ -61,12 +54,11 @@ class CallForegroundService : Service() {
|
|
|
61
54
|
.setOngoing(true)
|
|
62
55
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
63
56
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
64
|
-
.setWhen(System.currentTimeMillis())
|
|
65
57
|
.build()
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
private fun buildEnhancedNotification(callId: String, callType: String, displayName: String, state: String): Notification {
|
|
69
|
-
Log.d(TAG, "Building
|
|
61
|
+
Log.d(TAG, "Building notification for callId: $callId, state: $state")
|
|
70
62
|
|
|
71
63
|
val endCallIntent = Intent(this, CallNotificationActionReceiver::class.java).apply {
|
|
72
64
|
action = "com.qusaieilouti99.callmanager.END_CALL"
|
|
@@ -90,16 +82,6 @@ class CallForegroundService : Service() {
|
|
|
90
82
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
91
83
|
)
|
|
92
84
|
|
|
93
|
-
val mainIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
|
|
94
|
-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
95
|
-
}
|
|
96
|
-
val mainPendingIntent = mainIntent?.let {
|
|
97
|
-
PendingIntent.getActivity(
|
|
98
|
-
this, 102, it,
|
|
99
|
-
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
85
|
val statusText = when (state) {
|
|
104
86
|
"ACTIVE" -> displayName
|
|
105
87
|
"HELD" -> "$displayName (on hold)"
|
|
@@ -123,8 +105,8 @@ class CallForegroundService : Service() {
|
|
|
123
105
|
.setOngoing(true)
|
|
124
106
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
125
107
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
126
|
-
.setWhen(System.currentTimeMillis())
|
|
127
108
|
|
|
109
|
+
// Add action buttons for ACTIVE and HELD calls
|
|
128
110
|
if (state == "ACTIVE" || state == "HELD") {
|
|
129
111
|
notificationBuilder
|
|
130
112
|
.addAction(
|
|
@@ -145,10 +127,6 @@ class CallForegroundService : Service() {
|
|
|
145
127
|
)
|
|
146
128
|
}
|
|
147
129
|
|
|
148
|
-
mainPendingIntent?.let {
|
|
149
|
-
notificationBuilder.setContentIntent(it)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
130
|
return notificationBuilder.build()
|
|
153
131
|
}
|
|
154
132
|
|
|
@@ -166,37 +144,28 @@ class CallForegroundService : Service() {
|
|
|
166
144
|
|
|
167
145
|
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
168
146
|
manager.createNotificationChannel(channel)
|
|
169
|
-
Log.d(TAG, "Foreground notification channel '$CHANNEL_ID' created/updated.")
|
|
170
147
|
}
|
|
171
148
|
}
|
|
172
149
|
|
|
173
150
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
|
174
|
-
// Figure out which Activity’s task was just removed:
|
|
175
151
|
val removed = rootIntent?.component?.className
|
|
176
|
-
Log.d(TAG, "onTaskRemoved:
|
|
152
|
+
Log.d(TAG, "onTaskRemoved: $removed")
|
|
177
153
|
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return
|
|
154
|
+
// Only terminate if main app was removed, not CallActivity
|
|
155
|
+
if (removed != CallActivity::class.java.name) {
|
|
156
|
+
Log.d(TAG, "Main app task removed - terminating")
|
|
157
|
+
CallEngine.onApplicationTerminate()
|
|
183
158
|
}
|
|
184
159
|
|
|
185
|
-
// Otherwise (e.g. MainActivity removed), tear everything down:
|
|
186
|
-
Log.d(TAG, "Main task removed; ending all calls.")
|
|
187
|
-
CallEngine.onApplicationTerminate()
|
|
188
|
-
stopSelf()
|
|
189
160
|
super.onTaskRemoved(rootIntent)
|
|
190
|
-
|
|
161
|
+
}
|
|
191
162
|
|
|
192
163
|
override fun onDestroy() {
|
|
193
164
|
super.onDestroy()
|
|
194
|
-
Log.d(TAG, "Service onDestroy
|
|
165
|
+
Log.d(TAG, "Service onDestroy")
|
|
195
166
|
stopForeground(true)
|
|
196
167
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
CallEngine.onApplicationTerminate()
|
|
200
|
-
}
|
|
168
|
+
// SIMPLIFIED: Don't call onApplicationTerminate here
|
|
169
|
+
// Only onTaskRemoved should trigger app termination
|
|
201
170
|
}
|
|
202
171
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type CallEventType = 'AUDIO_DEVICES_CHANGED' | 'AUDIO_ROUTE_CHANGED' | 'CALL_HELD' | 'CALL_UNHELD' | 'CALL_UNHOLD_FAILED' | 'CALL_MUTED' | 'CALL_UNMUTED' | 'CALL_ANSWERED' | 'CALL_REJECTED' | 'CALL_ENDED' | 'DTMF_TONE';
|
|
1
|
+
export type CallEventType = 'AUDIO_DEVICES_CHANGED' | 'AUDIO_ROUTE_CHANGED' | 'CALL_HELD' | 'CALL_UNHELD' | 'CALL_UNHOLD_FAILED' | 'CALL_MUTED' | 'CALL_UNMUTED' | 'CALL_ANSWERED' | 'OUTGOING_CALL_ANSWERED' | 'CALL_REJECTED' | 'CALL_ENDED' | 'DTMF_TONE';
|
|
2
2
|
//# sourceMappingURL=CallEventType.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CallEventType.d.ts","sourceRoot":"","sources":["../../../src/CallEventType.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,aAAa,GACvB,uBAAuB,GACrB,qBAAqB,GACrB,WAAW,GACX,aAAa,GACb,oBAAoB,GACpB,YAAY,GACZ,cAAc,GACd,eAAe,GACf,eAAe,GACf,YAAY,GACZ,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"CallEventType.d.ts","sourceRoot":"","sources":["../../../src/CallEventType.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,aAAa,GACvB,uBAAuB,GACrB,qBAAqB,GACrB,WAAW,GACX,aAAa,GACb,oBAAoB,GACpB,YAAY,GACZ,cAAc,GACd,eAAe,GACf,wBAAwB,GACxB,eAAe,GACf,YAAY,GACZ,WAAW,CAAC"}
|
|
@@ -49,6 +49,7 @@ namespace margelo::nitro::qusaieilouti99_callmanager {
|
|
|
49
49
|
static const auto fieldCALL_MUTED = clazz->getStaticField<JCallEventType>("CALL_MUTED");
|
|
50
50
|
static const auto fieldCALL_UNMUTED = clazz->getStaticField<JCallEventType>("CALL_UNMUTED");
|
|
51
51
|
static const auto fieldCALL_ANSWERED = clazz->getStaticField<JCallEventType>("CALL_ANSWERED");
|
|
52
|
+
static const auto fieldOUTGOING_CALL_ANSWERED = clazz->getStaticField<JCallEventType>("OUTGOING_CALL_ANSWERED");
|
|
52
53
|
static const auto fieldCALL_REJECTED = clazz->getStaticField<JCallEventType>("CALL_REJECTED");
|
|
53
54
|
static const auto fieldCALL_ENDED = clazz->getStaticField<JCallEventType>("CALL_ENDED");
|
|
54
55
|
static const auto fieldDTMF_TONE = clazz->getStaticField<JCallEventType>("DTMF_TONE");
|
|
@@ -70,6 +71,8 @@ namespace margelo::nitro::qusaieilouti99_callmanager {
|
|
|
70
71
|
return clazz->getStaticFieldValue(fieldCALL_UNMUTED);
|
|
71
72
|
case CallEventType::CALL_ANSWERED:
|
|
72
73
|
return clazz->getStaticFieldValue(fieldCALL_ANSWERED);
|
|
74
|
+
case CallEventType::OUTGOING_CALL_ANSWERED:
|
|
75
|
+
return clazz->getStaticFieldValue(fieldOUTGOING_CALL_ANSWERED);
|
|
73
76
|
case CallEventType::CALL_REJECTED:
|
|
74
77
|
return clazz->getStaticFieldValue(fieldCALL_REJECTED);
|
|
75
78
|
case CallEventType::CALL_ENDED:
|
|
@@ -33,6 +33,8 @@ public extension CallEventType {
|
|
|
33
33
|
self = .callUnmuted
|
|
34
34
|
case "CALL_ANSWERED":
|
|
35
35
|
self = .callAnswered
|
|
36
|
+
case "OUTGOING_CALL_ANSWERED":
|
|
37
|
+
self = .outgoingCallAnswered
|
|
36
38
|
case "CALL_REJECTED":
|
|
37
39
|
self = .callRejected
|
|
38
40
|
case "CALL_ENDED":
|
|
@@ -65,6 +67,8 @@ public extension CallEventType {
|
|
|
65
67
|
return "CALL_UNMUTED"
|
|
66
68
|
case .callAnswered:
|
|
67
69
|
return "CALL_ANSWERED"
|
|
70
|
+
case .outgoingCallAnswered:
|
|
71
|
+
return "OUTGOING_CALL_ANSWERED"
|
|
68
72
|
case .callRejected:
|
|
69
73
|
return "CALL_REJECTED"
|
|
70
74
|
case .callEnded:
|
|
@@ -37,9 +37,10 @@ namespace margelo::nitro::qusaieilouti99_callmanager {
|
|
|
37
37
|
CALL_MUTED SWIFT_NAME(callMuted) = 5,
|
|
38
38
|
CALL_UNMUTED SWIFT_NAME(callUnmuted) = 6,
|
|
39
39
|
CALL_ANSWERED SWIFT_NAME(callAnswered) = 7,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
OUTGOING_CALL_ANSWERED SWIFT_NAME(outgoingCallAnswered) = 8,
|
|
41
|
+
CALL_REJECTED SWIFT_NAME(callRejected) = 9,
|
|
42
|
+
CALL_ENDED SWIFT_NAME(callEnded) = 10,
|
|
43
|
+
DTMF_TONE SWIFT_NAME(dtmfTone) = 11,
|
|
43
44
|
} CLOSED_ENUM;
|
|
44
45
|
|
|
45
46
|
} // namespace margelo::nitro::qusaieilouti99_callmanager
|
|
@@ -62,6 +63,7 @@ namespace margelo::nitro {
|
|
|
62
63
|
case hashString("CALL_MUTED"): return CallEventType::CALL_MUTED;
|
|
63
64
|
case hashString("CALL_UNMUTED"): return CallEventType::CALL_UNMUTED;
|
|
64
65
|
case hashString("CALL_ANSWERED"): return CallEventType::CALL_ANSWERED;
|
|
66
|
+
case hashString("OUTGOING_CALL_ANSWERED"): return CallEventType::OUTGOING_CALL_ANSWERED;
|
|
65
67
|
case hashString("CALL_REJECTED"): return CallEventType::CALL_REJECTED;
|
|
66
68
|
case hashString("CALL_ENDED"): return CallEventType::CALL_ENDED;
|
|
67
69
|
case hashString("DTMF_TONE"): return CallEventType::DTMF_TONE;
|
|
@@ -79,6 +81,7 @@ namespace margelo::nitro {
|
|
|
79
81
|
case CallEventType::CALL_MUTED: return JSIConverter<std::string>::toJSI(runtime, "CALL_MUTED");
|
|
80
82
|
case CallEventType::CALL_UNMUTED: return JSIConverter<std::string>::toJSI(runtime, "CALL_UNMUTED");
|
|
81
83
|
case CallEventType::CALL_ANSWERED: return JSIConverter<std::string>::toJSI(runtime, "CALL_ANSWERED");
|
|
84
|
+
case CallEventType::OUTGOING_CALL_ANSWERED: return JSIConverter<std::string>::toJSI(runtime, "OUTGOING_CALL_ANSWERED");
|
|
82
85
|
case CallEventType::CALL_REJECTED: return JSIConverter<std::string>::toJSI(runtime, "CALL_REJECTED");
|
|
83
86
|
case CallEventType::CALL_ENDED: return JSIConverter<std::string>::toJSI(runtime, "CALL_ENDED");
|
|
84
87
|
case CallEventType::DTMF_TONE: return JSIConverter<std::string>::toJSI(runtime, "DTMF_TONE");
|
|
@@ -101,6 +104,7 @@ namespace margelo::nitro {
|
|
|
101
104
|
case hashString("CALL_MUTED"):
|
|
102
105
|
case hashString("CALL_UNMUTED"):
|
|
103
106
|
case hashString("CALL_ANSWERED"):
|
|
107
|
+
case hashString("OUTGOING_CALL_ANSWERED"):
|
|
104
108
|
case hashString("CALL_REJECTED"):
|
|
105
109
|
case hashString("CALL_ENDED"):
|
|
106
110
|
case hashString("DTMF_TONE"):
|
package/package.json
CHANGED