@qusaieilouti99/call-manager 0.1.65 → 0.1.67
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.
|
@@ -31,6 +31,7 @@ import android.telecom.PhoneAccountHandle
|
|
|
31
31
|
import android.telecom.TelecomManager
|
|
32
32
|
import android.telecom.VideoProfile
|
|
33
33
|
import android.util.Log
|
|
34
|
+
import androidx.annotation.RequiresApi
|
|
34
35
|
import kotlinx.coroutines.CoroutineScope
|
|
35
36
|
import kotlinx.coroutines.Dispatchers
|
|
36
37
|
import kotlinx.coroutines.launch
|
|
@@ -44,8 +45,6 @@ object CallEngine {
|
|
|
44
45
|
private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
|
|
45
46
|
private const val NOTIF_CHANNEL_ID = "incoming_call_channel"
|
|
46
47
|
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
48
|
|
|
50
49
|
// Core context - initialized once and maintained
|
|
51
50
|
@Volatile
|
|
@@ -53,12 +52,16 @@ object CallEngine {
|
|
|
53
52
|
private val isInitialized = AtomicBoolean(false)
|
|
54
53
|
private val initializationLock = Any()
|
|
55
54
|
|
|
56
|
-
// Audio & Media
|
|
55
|
+
// Enhanced Audio & Media Management
|
|
57
56
|
private var ringtone: android.media.Ringtone? = null
|
|
58
57
|
private var ringbackPlayer: MediaPlayer? = null
|
|
59
58
|
private var audioManager: AudioManager? = null
|
|
60
59
|
private var wakeLock: PowerManager.WakeLock? = null
|
|
61
60
|
private var audioFocusRequest: AudioFocusRequest? = null
|
|
61
|
+
private var hasAudioFocus: Boolean = false
|
|
62
|
+
private val audioFocusRetryHandler = Handler(Looper.getMainLooper())
|
|
63
|
+
private var audioFocusRetryCount = 0
|
|
64
|
+
private val MAX_AUDIO_FOCUS_RETRIES = 3
|
|
62
65
|
|
|
63
66
|
// Call State Management
|
|
64
67
|
private val activeCalls = ConcurrentHashMap<String, CallInfo>()
|
|
@@ -70,8 +73,6 @@ object CallEngine {
|
|
|
70
73
|
|
|
71
74
|
// Audio State Tracking
|
|
72
75
|
private var lastAudioRoutesInfo: AudioRoutesInfo? = null
|
|
73
|
-
private var hasAudioFocus: Boolean = false
|
|
74
|
-
private var isSystemCallActive: Boolean = false
|
|
75
76
|
|
|
76
77
|
// Lock Screen Bypass
|
|
77
78
|
private var lockScreenBypassActive = false
|
|
@@ -85,25 +86,49 @@ object CallEngine {
|
|
|
85
86
|
fun onLockScreenBypassChanged(shouldBypass: Boolean)
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
//
|
|
89
|
+
// Enhanced Audio Focus Change Listener for Self-Managed Calls
|
|
90
|
+
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
|
91
|
+
Log.d(TAG, "Audio focus changed: $focusChange")
|
|
92
|
+
when (focusChange) {
|
|
93
|
+
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
94
|
+
Log.d(TAG, "Audio focus gained")
|
|
95
|
+
hasAudioFocus = true
|
|
96
|
+
audioFocusRetryCount = 0
|
|
97
|
+
|
|
98
|
+
// Resume any system-held calls after a short delay
|
|
99
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
100
|
+
resumeSystemHeldCalls()
|
|
101
|
+
}, 500)
|
|
102
|
+
}
|
|
103
|
+
AudioManager.AUDIOFOCUS_LOSS -> {
|
|
104
|
+
Log.d(TAG, "Permanent audio focus loss - holding active calls")
|
|
105
|
+
hasAudioFocus = false
|
|
106
|
+
holdAllActiveCalls(heldBySystem = true)
|
|
107
|
+
}
|
|
108
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
|
109
|
+
Log.d(TAG, "Transient audio focus loss - holding calls temporarily")
|
|
110
|
+
hasAudioFocus = false
|
|
111
|
+
holdAllActiveCalls(heldBySystem = true)
|
|
112
|
+
}
|
|
113
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
|
114
|
+
Log.d(TAG, "Transient audio focus loss (can duck) - keeping calls active")
|
|
115
|
+
hasAudioFocus = false
|
|
116
|
+
// Don't hold calls for ducking scenarios in self-managed calls
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- INITIALIZATION ---
|
|
89
122
|
fun initialize(context: Context) {
|
|
90
123
|
synchronized(initializationLock) {
|
|
91
124
|
if (isInitialized.compareAndSet(false, true)) {
|
|
92
125
|
appContext = context.applicationContext
|
|
93
126
|
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
|
-
}
|
|
127
|
+
Log.d(TAG, "CallEngine initialized successfully")
|
|
100
128
|
|
|
101
|
-
// Initialize foreground service if needed
|
|
102
129
|
if (isCallActive()) {
|
|
103
130
|
startForegroundService()
|
|
104
131
|
}
|
|
105
|
-
} else {
|
|
106
|
-
Log.d(TAG, "CallEngine already initialized, skipping")
|
|
107
132
|
}
|
|
108
133
|
}
|
|
109
134
|
}
|
|
@@ -112,7 +137,7 @@ object CallEngine {
|
|
|
112
137
|
|
|
113
138
|
private fun requireContext(): Context {
|
|
114
139
|
return appContext ?: throw IllegalStateException(
|
|
115
|
-
"CallEngine not initialized.
|
|
140
|
+
"CallEngine not initialized. Call initialize() in Application.onCreate()"
|
|
116
141
|
)
|
|
117
142
|
}
|
|
118
143
|
|
|
@@ -129,142 +154,111 @@ object CallEngine {
|
|
|
129
154
|
}
|
|
130
155
|
}
|
|
131
156
|
|
|
132
|
-
// Made public for MyConnection
|
|
133
157
|
fun emitEvent(type: CallEventType, data: JSONObject) {
|
|
134
|
-
Log.d(TAG, "Emitting event: $type
|
|
158
|
+
Log.d(TAG, "Emitting event: $type")
|
|
135
159
|
val dataString = data.toString()
|
|
136
160
|
if (eventHandler != null) {
|
|
137
161
|
eventHandler?.invoke(type, dataString)
|
|
138
162
|
} else {
|
|
139
|
-
Log.d(TAG, "No event handler
|
|
163
|
+
Log.d(TAG, "No event handler, caching event: $type")
|
|
140
164
|
cachedEvents.add(Pair(type, dataString))
|
|
141
165
|
}
|
|
142
166
|
}
|
|
143
167
|
|
|
144
|
-
// ---
|
|
145
|
-
private
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
AudioManager.AUDIOFOCUS_LOSS -> {
|
|
149
|
-
Log.d(TAG, "Permanent audio focus loss - another app took focus")
|
|
150
|
-
hasAudioFocus = false
|
|
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
|
|
163
|
-
hasAudioFocus = false
|
|
164
|
-
}
|
|
165
|
-
AudioManager.AUDIOFOCUS_GAIN -> {
|
|
166
|
-
Log.d(TAG, "Audio focus gained")
|
|
167
|
-
hasAudioFocus = true
|
|
168
|
-
isSystemCallActive = false
|
|
169
|
-
// Delay resuming to avoid rapid hold/unhold cycles
|
|
170
|
-
Handler(Looper.getMainLooper()).postDelayed({
|
|
171
|
-
resumeSystemHeldCalls()
|
|
172
|
-
}, 500) // Reduced from 1000ms
|
|
173
|
-
}
|
|
174
|
-
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> {
|
|
175
|
-
Log.d(TAG, "Transient audio focus gained")
|
|
176
|
-
hasAudioFocus = true
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
updateForegroundNotification()
|
|
180
|
-
}
|
|
168
|
+
// --- Enhanced Audio Focus Management for Self-Managed Calls ---
|
|
169
|
+
private fun requestAudioFocus(): Boolean {
|
|
170
|
+
val context = requireContext()
|
|
171
|
+
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
181
172
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
173
|
+
if (hasAudioFocus) {
|
|
174
|
+
Log.d(TAG, "Audio focus already granted")
|
|
175
|
+
return true
|
|
185
176
|
}
|
|
186
177
|
|
|
187
|
-
|
|
188
|
-
Log.d(TAG, "No active calls to hold due to audio focus loss")
|
|
189
|
-
return
|
|
190
|
-
}
|
|
178
|
+
Log.d(TAG, "Requesting audio focus for self-managed call (attempt ${audioFocusRetryCount + 1})")
|
|
191
179
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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}")
|
|
200
|
-
}
|
|
180
|
+
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
181
|
+
requestAudioFocusApi26Plus()
|
|
182
|
+
} else {
|
|
183
|
+
requestAudioFocusLegacy()
|
|
201
184
|
}
|
|
202
|
-
stopRingback()
|
|
203
|
-
}
|
|
204
185
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
it.state == CallState.HELD && it.wasHeldBySystem
|
|
208
|
-
}
|
|
186
|
+
val success = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
187
|
+
hasAudioFocus = success
|
|
209
188
|
|
|
210
|
-
|
|
211
|
-
Log.d(TAG, "No system-held calls to resume")
|
|
212
|
-
return
|
|
213
|
-
}
|
|
189
|
+
Log.d(TAG, "Audio focus request result: $result (granted: $success)")
|
|
214
190
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
191
|
+
if (!success && audioFocusRetryCount < MAX_AUDIO_FOCUS_RETRIES) {
|
|
192
|
+
// Retry after a short delay
|
|
193
|
+
audioFocusRetryCount++
|
|
194
|
+
audioFocusRetryHandler.postDelayed({
|
|
195
|
+
Log.d(TAG, "Retrying audio focus request...")
|
|
196
|
+
requestAudioFocus()
|
|
197
|
+
}, 200)
|
|
218
198
|
}
|
|
219
|
-
}
|
|
220
199
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
|
|
200
|
+
return success
|
|
201
|
+
}
|
|
224
202
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
Log.d(TAG, "Audio focus request result: $result (granted: $hasAudioFocus)")
|
|
241
|
-
hasAudioFocus
|
|
242
|
-
} else {
|
|
243
|
-
@Suppress("DEPRECATION")
|
|
244
|
-
val result = audioManager?.requestAudioFocus(
|
|
245
|
-
audioFocusChangeListener,
|
|
246
|
-
AudioManager.STREAM_VOICE_CALL,
|
|
247
|
-
AudioManager.AUDIOFOCUS_GAIN
|
|
248
|
-
)
|
|
249
|
-
hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
|
|
250
|
-
Log.d(TAG, "Audio focus request result (legacy): $result (granted: $hasAudioFocus)")
|
|
251
|
-
hasAudioFocus
|
|
203
|
+
@RequiresApi(Build.VERSION_CODES.O)
|
|
204
|
+
private fun requestAudioFocusApi26Plus(): Int {
|
|
205
|
+
if (audioFocusRequest == null) {
|
|
206
|
+
audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
|
|
207
|
+
.setAudioAttributes(
|
|
208
|
+
AudioAttributes.Builder()
|
|
209
|
+
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
|
|
210
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
211
|
+
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
|
|
212
|
+
.build()
|
|
213
|
+
)
|
|
214
|
+
.setOnAudioFocusChangeListener(audioFocusChangeListener)
|
|
215
|
+
.setAcceptsDelayedFocusGain(true)
|
|
216
|
+
.setWillPauseWhenDucked(false)
|
|
217
|
+
.build()
|
|
252
218
|
}
|
|
219
|
+
|
|
220
|
+
return audioManager?.requestAudioFocus(audioFocusRequest!!) ?: AudioManager.AUDIOFOCUS_REQUEST_FAILED
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@Suppress("DEPRECATION")
|
|
224
|
+
private fun requestAudioFocusLegacy(): Int {
|
|
225
|
+
return audioManager?.requestAudioFocus(
|
|
226
|
+
audioFocusChangeListener,
|
|
227
|
+
AudioManager.STREAM_VOICE_CALL,
|
|
228
|
+
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE
|
|
229
|
+
) ?: AudioManager.AUDIOFOCUS_REQUEST_FAILED
|
|
253
230
|
}
|
|
254
231
|
|
|
255
232
|
private fun abandonAudioFocus() {
|
|
233
|
+
if (!hasAudioFocus) return
|
|
234
|
+
|
|
256
235
|
audioManager?.let { am ->
|
|
257
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
236
|
+
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
258
237
|
audioFocusRequest?.let { request ->
|
|
259
238
|
am.abandonAudioFocusRequest(request)
|
|
260
|
-
}
|
|
239
|
+
} ?: AudioManager.AUDIOFOCUS_REQUEST_FAILED
|
|
261
240
|
} else {
|
|
262
241
|
@Suppress("DEPRECATION")
|
|
263
242
|
am.abandonAudioFocus(audioFocusChangeListener)
|
|
264
243
|
}
|
|
244
|
+
Log.d(TAG, "Audio focus abandoned, result: $result")
|
|
265
245
|
}
|
|
246
|
+
|
|
266
247
|
hasAudioFocus = false
|
|
267
|
-
|
|
248
|
+
audioFocusRetryCount = 0
|
|
249
|
+
audioFocusRetryHandler.removeCallbacksAndMessages(null)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private fun holdAllActiveCalls(heldBySystem: Boolean) {
|
|
253
|
+
activeCalls.values.filter { it.state == CallState.ACTIVE }.forEach { call ->
|
|
254
|
+
holdCallInternal(call.callId, heldBySystem = heldBySystem)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private fun resumeSystemHeldCalls() {
|
|
259
|
+
activeCalls.values.filter { it.state == CallState.HELD && it.wasHeldBySystem }.forEach { call ->
|
|
260
|
+
unholdCallInternal(call.callId, resumedBySystem = true)
|
|
261
|
+
}
|
|
268
262
|
}
|
|
269
263
|
|
|
270
264
|
// --- Lock Screen Bypass Management ---
|
|
@@ -296,12 +290,12 @@ object CallEngine {
|
|
|
296
290
|
// --- Telecom Connection Management ---
|
|
297
291
|
fun addTelecomConnection(callId: String, connection: Connection) {
|
|
298
292
|
telecomConnections[callId] = connection
|
|
299
|
-
Log.d(TAG, "Added Telecom Connection for callId: $callId
|
|
293
|
+
Log.d(TAG, "Added Telecom Connection for callId: $callId")
|
|
300
294
|
}
|
|
301
295
|
|
|
302
296
|
fun removeTelecomConnection(callId: String) {
|
|
303
297
|
telecomConnections.remove(callId)?.let {
|
|
304
|
-
Log.d(TAG, "Removed Telecom Connection for callId: $callId
|
|
298
|
+
Log.d(TAG, "Removed Telecom Connection for callId: $callId")
|
|
305
299
|
}
|
|
306
300
|
}
|
|
307
301
|
|
|
@@ -319,9 +313,7 @@ object CallEngine {
|
|
|
319
313
|
calls.forEach {
|
|
320
314
|
jsonArray.put(it.toJsonObject())
|
|
321
315
|
}
|
|
322
|
-
|
|
323
|
-
Log.d(TAG, "Current call state: $result")
|
|
324
|
-
return result
|
|
316
|
+
return jsonArray.toString()
|
|
325
317
|
}
|
|
326
318
|
|
|
327
319
|
// --- Incoming Call Management ---
|
|
@@ -368,7 +360,7 @@ object CallEngine {
|
|
|
368
360
|
|
|
369
361
|
activeCalls[callId] = CallInfo(callId, callType, displayName, pictureUrl, CallState.INCOMING)
|
|
370
362
|
currentCallId = callId
|
|
371
|
-
Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING
|
|
363
|
+
Log.d(TAG, "Call $callId added to activeCalls. State: INCOMING")
|
|
372
364
|
|
|
373
365
|
showIncomingCallUI(callId, displayName, callType)
|
|
374
366
|
registerPhoneAccount()
|
|
@@ -387,9 +379,6 @@ object CallEngine {
|
|
|
387
379
|
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
|
|
388
380
|
startForegroundService()
|
|
389
381
|
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
382
|
} catch (e: Exception) {
|
|
394
383
|
Log.e(TAG, "Failed to report incoming call: ${e.message}", e)
|
|
395
384
|
endCallInternal(callId)
|
|
@@ -398,7 +387,7 @@ object CallEngine {
|
|
|
398
387
|
updateLockScreenBypass()
|
|
399
388
|
}
|
|
400
389
|
|
|
401
|
-
// --- Outgoing Call Management ---
|
|
390
|
+
// --- Enhanced Outgoing Call Management ---
|
|
402
391
|
fun startOutgoingCall(
|
|
403
392
|
callId: String,
|
|
404
393
|
callType: String,
|
|
@@ -430,12 +419,16 @@ object CallEngine {
|
|
|
430
419
|
currentCallId = callId
|
|
431
420
|
Log.d(TAG, "Call $callId added to activeCalls. State: DIALING")
|
|
432
421
|
|
|
422
|
+
// Set audio mode and request focus early for outgoing calls
|
|
423
|
+
setAudioMode()
|
|
424
|
+
val audioFocusGranted = requestAudioFocus()
|
|
425
|
+
Log.d(TAG, "Audio focus for outgoing call: $audioFocusGranted")
|
|
426
|
+
|
|
433
427
|
registerPhoneAccount()
|
|
434
428
|
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
435
429
|
val phoneAccountHandle = getPhoneAccountHandle()
|
|
436
430
|
val addressUri = Uri.fromParts(PhoneAccount.SCHEME_TEL, targetName, null)
|
|
437
431
|
|
|
438
|
-
// Build a bundle of ONLY your own keys
|
|
439
432
|
val outgoingExtras = Bundle().apply {
|
|
440
433
|
putString(MyConnectionService.EXTRA_CALL_ID, callId)
|
|
441
434
|
putString(MyConnectionService.EXTRA_CALL_TYPE, callType)
|
|
@@ -444,7 +437,6 @@ object CallEngine {
|
|
|
444
437
|
metadata?.let { putString("metadata", it) }
|
|
445
438
|
}
|
|
446
439
|
|
|
447
|
-
// Wrap under the single Telecom-honored key
|
|
448
440
|
val extras = Bundle().apply {
|
|
449
441
|
putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle)
|
|
450
442
|
putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, outgoingExtras)
|
|
@@ -455,21 +447,15 @@ object CallEngine {
|
|
|
455
447
|
telecomManager.placeCall(addressUri, extras)
|
|
456
448
|
startForegroundService()
|
|
457
449
|
|
|
458
|
-
//
|
|
459
|
-
val audioFocusGranted = requestAudioFocus()
|
|
450
|
+
// Start ringback only if audio focus is available
|
|
460
451
|
if (audioFocusGranted) {
|
|
461
452
|
startRingback()
|
|
462
|
-
} else {
|
|
463
|
-
Log.w(TAG, "Audio focus not granted for outgoing call, skipping ringback")
|
|
464
453
|
}
|
|
465
454
|
|
|
466
455
|
bringAppToForeground()
|
|
467
456
|
keepScreenAwake(true)
|
|
468
457
|
setInitialAudioRoute(callType)
|
|
469
458
|
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
459
|
} catch (e: Exception) {
|
|
474
460
|
Log.e(TAG, "Failed to start outgoing call: ${e.message}", e)
|
|
475
461
|
endCallInternal(callId)
|
|
@@ -478,14 +464,12 @@ object CallEngine {
|
|
|
478
464
|
updateLockScreenBypass()
|
|
479
465
|
}
|
|
480
466
|
|
|
481
|
-
// Fixed: Start call as active (not dialing) with foreground service
|
|
482
467
|
fun startCall(
|
|
483
468
|
callId: String,
|
|
484
469
|
callType: String,
|
|
485
470
|
targetName: String,
|
|
486
471
|
metadata: String? = null
|
|
487
472
|
) {
|
|
488
|
-
val context = requireContext()
|
|
489
473
|
Log.d(TAG, "startCall: callId=$callId, type=$callType, target=$targetName")
|
|
490
474
|
|
|
491
475
|
metadata?.let { callMetadata[callId] = it }
|
|
@@ -506,9 +490,10 @@ object CallEngine {
|
|
|
506
490
|
// Start directly as active call
|
|
507
491
|
activeCalls[callId] = CallInfo(callId, callType, targetName, null, CallState.ACTIVE)
|
|
508
492
|
currentCallId = callId
|
|
509
|
-
Log.d(TAG, "Call $callId started as ACTIVE
|
|
493
|
+
Log.d(TAG, "Call $callId started as ACTIVE")
|
|
510
494
|
|
|
511
495
|
registerPhoneAccount()
|
|
496
|
+
setAudioMode()
|
|
512
497
|
requestAudioFocus()
|
|
513
498
|
bringAppToForeground()
|
|
514
499
|
startForegroundService()
|
|
@@ -516,11 +501,11 @@ object CallEngine {
|
|
|
516
501
|
setInitialAudioRoute(callType)
|
|
517
502
|
updateLockScreenBypass()
|
|
518
503
|
|
|
519
|
-
// Emit outgoing call answered event
|
|
504
|
+
// Emit outgoing call answered event
|
|
520
505
|
emitOutgoingCallAnsweredWithMetadata(callId)
|
|
521
506
|
}
|
|
522
507
|
|
|
523
|
-
// --- Call Answer Management ---
|
|
508
|
+
// --- Enhanced Call Answer Management ---
|
|
524
509
|
fun callAnsweredFromJS(callId: String) {
|
|
525
510
|
Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
|
|
526
511
|
coreCallAnswered(callId, isLocalAnswer = false)
|
|
@@ -531,9 +516,8 @@ object CallEngine {
|
|
|
531
516
|
coreCallAnswered(callId, isLocalAnswer = true)
|
|
532
517
|
}
|
|
533
518
|
|
|
534
|
-
//
|
|
519
|
+
// Enhanced call answer flow with proper audio focus timing
|
|
535
520
|
private fun coreCallAnswered(callId: String, isLocalAnswer: Boolean) {
|
|
536
|
-
val context = requireContext()
|
|
537
521
|
Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
|
|
538
522
|
|
|
539
523
|
val callInfo = activeCalls[callId]
|
|
@@ -542,28 +526,26 @@ object CallEngine {
|
|
|
542
526
|
return
|
|
543
527
|
}
|
|
544
528
|
|
|
545
|
-
//
|
|
529
|
+
// Set audio mode BEFORE requesting audio focus
|
|
530
|
+
setAudioMode()
|
|
531
|
+
|
|
532
|
+
// Request audio focus BEFORE setting call to active
|
|
546
533
|
val audioFocusGranted = requestAudioFocus()
|
|
547
534
|
if (!audioFocusGranted) {
|
|
548
|
-
Log.w(TAG, "
|
|
549
|
-
// Don't fail the call, but warn about audio issues
|
|
535
|
+
Log.w(TAG, "Audio focus not granted for call $callId, but proceeding anyway")
|
|
550
536
|
}
|
|
551
537
|
|
|
552
|
-
//
|
|
538
|
+
// Now set call to ACTIVE
|
|
539
|
+
activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
|
|
540
|
+
currentCallId = callId
|
|
541
|
+
Log.d(TAG, "Call $callId set to ACTIVE state (audio focus: $audioFocusGranted)")
|
|
542
|
+
|
|
543
|
+
// Clean up media and UI
|
|
553
544
|
stopRingtone()
|
|
554
545
|
stopRingback()
|
|
555
546
|
cancelIncomingCallUI()
|
|
556
547
|
|
|
557
|
-
//
|
|
558
|
-
if (audioFocusGranted || !isLocalAnswer) {
|
|
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
|
-
}
|
|
566
|
-
|
|
548
|
+
// Handle multiple calls
|
|
567
549
|
if (!canMakeMultipleCalls) {
|
|
568
550
|
activeCalls.filter { it.key != callId }.values.forEach { otherCall ->
|
|
569
551
|
if (otherCall.state == CallState.ACTIVE) {
|
|
@@ -575,20 +557,16 @@ object CallEngine {
|
|
|
575
557
|
bringAppToForeground()
|
|
576
558
|
startForegroundService()
|
|
577
559
|
keepScreenAwake(true)
|
|
578
|
-
resetAudioMode()
|
|
579
560
|
updateLockScreenBypass()
|
|
580
|
-
updateForegroundNotification()
|
|
581
561
|
|
|
582
|
-
//
|
|
562
|
+
// Emit events based on call direction
|
|
583
563
|
if (isLocalAnswer) {
|
|
584
|
-
// This is for incoming calls - user answered locally
|
|
585
564
|
emitCallAnsweredWithMetadata(callId)
|
|
586
565
|
} else {
|
|
587
|
-
// This is for outgoing calls - remote party answered
|
|
588
566
|
emitOutgoingCallAnsweredWithMetadata(callId)
|
|
589
567
|
}
|
|
590
568
|
|
|
591
|
-
Log.d(TAG, "Call $callId successfully answered
|
|
569
|
+
Log.d(TAG, "Call $callId successfully answered")
|
|
592
570
|
}
|
|
593
571
|
|
|
594
572
|
// For incoming calls (local answer)
|
|
@@ -605,8 +583,7 @@ object CallEngine {
|
|
|
605
583
|
try {
|
|
606
584
|
put("metadata", JSONObject(it))
|
|
607
585
|
} catch (e: Exception) {
|
|
608
|
-
|
|
609
|
-
put("metadata", it) // fallback to string
|
|
586
|
+
put("metadata", it)
|
|
610
587
|
}
|
|
611
588
|
}
|
|
612
589
|
})
|
|
@@ -626,8 +603,7 @@ object CallEngine {
|
|
|
626
603
|
try {
|
|
627
604
|
put("metadata", JSONObject(it))
|
|
628
605
|
} catch (e: Exception) {
|
|
629
|
-
|
|
630
|
-
put("metadata", it) // fallback to string
|
|
606
|
+
put("metadata", it)
|
|
631
607
|
}
|
|
632
608
|
}
|
|
633
609
|
})
|
|
@@ -640,21 +616,16 @@ object CallEngine {
|
|
|
640
616
|
|
|
641
617
|
fun setOnHold(callId: String, onHold: Boolean) {
|
|
642
618
|
Log.d(TAG, "setOnHold: $callId, onHold: $onHold")
|
|
643
|
-
|
|
644
619
|
val callInfo = activeCalls[callId]
|
|
645
620
|
if (callInfo == null) {
|
|
646
|
-
Log.w(TAG, "Cannot set hold state for call $callId - not found
|
|
621
|
+
Log.w(TAG, "Cannot set hold state for call $callId - not found")
|
|
647
622
|
return
|
|
648
623
|
}
|
|
649
624
|
|
|
650
|
-
if (onHold) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
} else {
|
|
655
|
-
if (callInfo.state == CallState.HELD) {
|
|
656
|
-
unholdCallInternal(callId, resumedBySystem = false)
|
|
657
|
-
}
|
|
625
|
+
if (onHold && callInfo.state == CallState.ACTIVE) {
|
|
626
|
+
holdCallInternal(callId, heldBySystem = false)
|
|
627
|
+
} else if (!onHold && callInfo.state == CallState.HELD) {
|
|
628
|
+
unholdCallInternal(callId, resumedBySystem = false)
|
|
658
629
|
}
|
|
659
630
|
}
|
|
660
631
|
|
|
@@ -662,7 +633,7 @@ object CallEngine {
|
|
|
662
633
|
Log.d(TAG, "holdCallInternal: $callId, heldBySystem: $heldBySystem")
|
|
663
634
|
val callInfo = activeCalls[callId]
|
|
664
635
|
if (callInfo?.state != CallState.ACTIVE) {
|
|
665
|
-
Log.w(TAG, "Cannot hold call $callId - not in active state
|
|
636
|
+
Log.w(TAG, "Cannot hold call $callId - not in active state")
|
|
666
637
|
return
|
|
667
638
|
}
|
|
668
639
|
|
|
@@ -671,9 +642,7 @@ object CallEngine {
|
|
|
671
642
|
wasHeldBySystem = heldBySystem
|
|
672
643
|
)
|
|
673
644
|
|
|
674
|
-
|
|
675
|
-
connection?.setOnHold()
|
|
676
|
-
|
|
645
|
+
telecomConnections[callId]?.setOnHold()
|
|
677
646
|
updateForegroundNotification()
|
|
678
647
|
emitEvent(CallEventType.CALL_HELD, JSONObject().put("callId", callId))
|
|
679
648
|
updateLockScreenBypass()
|
|
@@ -687,14 +656,13 @@ object CallEngine {
|
|
|
687
656
|
Log.d(TAG, "unholdCallInternal: $callId, resumedBySystem: $resumedBySystem")
|
|
688
657
|
val callInfo = activeCalls[callId]
|
|
689
658
|
if (callInfo?.state != CallState.HELD) {
|
|
690
|
-
Log.w(TAG, "Cannot unhold call $callId - not in held state
|
|
659
|
+
Log.w(TAG, "Cannot unhold call $callId - not in held state")
|
|
691
660
|
return
|
|
692
661
|
}
|
|
693
662
|
|
|
694
|
-
//
|
|
695
|
-
if (
|
|
696
|
-
|
|
697
|
-
// Don't emit UNHELD FAILED - just continue
|
|
663
|
+
// Request audio focus when resuming a call
|
|
664
|
+
if (resumedBySystem) {
|
|
665
|
+
requestAudioFocus()
|
|
698
666
|
}
|
|
699
667
|
|
|
700
668
|
activeCalls[callId] = callInfo.copy(
|
|
@@ -702,14 +670,10 @@ object CallEngine {
|
|
|
702
670
|
wasHeldBySystem = false
|
|
703
671
|
)
|
|
704
672
|
|
|
705
|
-
|
|
706
|
-
connection?.setActive()
|
|
707
|
-
|
|
673
|
+
telecomConnections[callId]?.setActive()
|
|
708
674
|
updateForegroundNotification()
|
|
709
675
|
emitEvent(CallEventType.CALL_UNHELD, JSONObject().put("callId", callId))
|
|
710
676
|
updateLockScreenBypass()
|
|
711
|
-
|
|
712
|
-
Log.d(TAG, "Call $callId successfully unheld")
|
|
713
677
|
}
|
|
714
678
|
|
|
715
679
|
fun muteCall(callId: String) {
|
|
@@ -721,18 +685,17 @@ object CallEngine {
|
|
|
721
685
|
}
|
|
722
686
|
|
|
723
687
|
fun setMuted(callId: String, muted: Boolean) {
|
|
724
|
-
Log.d(TAG, "setMuted: $callId, muted: $muted")
|
|
725
688
|
setMutedInternal(callId, muted)
|
|
726
689
|
}
|
|
727
690
|
|
|
728
691
|
private fun setMutedInternal(callId: String, muted: Boolean) {
|
|
729
|
-
val context = requireContext()
|
|
730
692
|
val callInfo = activeCalls[callId]
|
|
731
693
|
if (callInfo == null) {
|
|
732
|
-
Log.w(TAG, "Cannot set mute state for call $callId - not found
|
|
694
|
+
Log.w(TAG, "Cannot set mute state for call $callId - not found")
|
|
733
695
|
return
|
|
734
696
|
}
|
|
735
697
|
|
|
698
|
+
val context = requireContext()
|
|
736
699
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
737
700
|
|
|
738
701
|
val wasMuted = audioManager?.isMicrophoneMute ?: false
|
|
@@ -752,11 +715,8 @@ object CallEngine {
|
|
|
752
715
|
}
|
|
753
716
|
|
|
754
717
|
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
|
-
}
|
|
718
|
+
Log.d(TAG, "endAllCalls: Ending all active calls")
|
|
719
|
+
if (activeCalls.isEmpty()) return
|
|
760
720
|
|
|
761
721
|
activeCalls.keys.toList().forEach { callId ->
|
|
762
722
|
endCallInternal(callId)
|
|
@@ -767,7 +727,7 @@ object CallEngine {
|
|
|
767
727
|
callMetadata.clear()
|
|
768
728
|
currentCallId = null
|
|
769
729
|
|
|
770
|
-
|
|
730
|
+
cleanup()
|
|
771
731
|
updateLockScreenBypass()
|
|
772
732
|
}
|
|
773
733
|
|
|
@@ -779,12 +739,10 @@ object CallEngine {
|
|
|
779
739
|
return
|
|
780
740
|
}
|
|
781
741
|
|
|
782
|
-
// Get metadata before removing
|
|
783
742
|
val metadata = callMetadata.remove(callId)
|
|
784
743
|
|
|
785
744
|
activeCalls[callId] = callInfo.copy(state = CallState.ENDED)
|
|
786
745
|
activeCalls.remove(callId)
|
|
787
|
-
Log.d(TAG, "Call $callId removed from activeCalls. Remaining: ${activeCalls.size}")
|
|
788
746
|
|
|
789
747
|
stopRingback()
|
|
790
748
|
stopRingtone()
|
|
@@ -792,7 +750,6 @@ object CallEngine {
|
|
|
792
750
|
|
|
793
751
|
if (currentCallId == callId) {
|
|
794
752
|
currentCallId = activeCalls.filter { it.value.state != CallState.ENDED }.keys.firstOrNull()
|
|
795
|
-
Log.d(TAG, "Current call was $callId. New currentCallId: $currentCallId")
|
|
796
753
|
}
|
|
797
754
|
|
|
798
755
|
val connection = telecomConnections[callId]
|
|
@@ -800,11 +757,10 @@ object CallEngine {
|
|
|
800
757
|
connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
801
758
|
connection.destroy()
|
|
802
759
|
removeTelecomConnection(callId)
|
|
803
|
-
Log.d(TAG, "Telecom Connection for $callId disconnected and destroyed.")
|
|
804
760
|
}
|
|
805
761
|
|
|
806
762
|
if (activeCalls.isEmpty()) {
|
|
807
|
-
|
|
763
|
+
cleanup()
|
|
808
764
|
} else {
|
|
809
765
|
updateForegroundNotification()
|
|
810
766
|
}
|
|
@@ -818,8 +774,7 @@ object CallEngine {
|
|
|
818
774
|
try {
|
|
819
775
|
put("metadata", JSONObject(it))
|
|
820
776
|
} catch (e: Exception) {
|
|
821
|
-
|
|
822
|
-
put("metadata", it) // fallback to string
|
|
777
|
+
put("metadata", it)
|
|
823
778
|
}
|
|
824
779
|
}
|
|
825
780
|
})
|
|
@@ -829,7 +784,6 @@ object CallEngine {
|
|
|
829
784
|
fun getAudioDevices(): AudioRoutesInfo {
|
|
830
785
|
val context = requireContext()
|
|
831
786
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
|
|
832
|
-
Log.e(TAG, "getAudioDevices: AudioManager is null. Returning default.")
|
|
833
787
|
return AudioRoutesInfo(emptyArray(), "Unknown")
|
|
834
788
|
}
|
|
835
789
|
|
|
@@ -865,15 +819,13 @@ object CallEngine {
|
|
|
865
819
|
else -> "Earpiece"
|
|
866
820
|
}
|
|
867
821
|
|
|
868
|
-
|
|
869
|
-
Log.d(TAG, "Audio devices info: $result")
|
|
870
|
-
return result
|
|
822
|
+
return AudioRoutesInfo(devices.toTypedArray(), currentRoute)
|
|
871
823
|
}
|
|
872
824
|
|
|
873
825
|
fun setAudioRoute(route: String) {
|
|
874
826
|
val context = requireContext()
|
|
875
827
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
876
|
-
Log.d(TAG, "
|
|
828
|
+
Log.d(TAG, "Setting audio route to: $route")
|
|
877
829
|
|
|
878
830
|
val previousRoute = getCurrentAudioRoute()
|
|
879
831
|
|
|
@@ -883,26 +835,22 @@ object CallEngine {
|
|
|
883
835
|
|
|
884
836
|
when (route) {
|
|
885
837
|
"Speaker" -> {
|
|
886
|
-
Log.d(TAG, "Setting audio route to Speaker.")
|
|
887
838
|
audioManager?.isSpeakerphoneOn = true
|
|
888
839
|
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
889
840
|
}
|
|
890
841
|
"Earpiece" -> {
|
|
891
|
-
Log.d(TAG, "Setting audio route to Earpiece.")
|
|
892
842
|
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
893
843
|
}
|
|
894
844
|
"Bluetooth" -> {
|
|
895
|
-
Log.d(TAG, "Setting audio route to Bluetooth.")
|
|
896
845
|
audioManager?.startBluetoothSco()
|
|
897
846
|
audioManager?.isBluetoothScoOn = true
|
|
898
847
|
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
899
848
|
}
|
|
900
849
|
"Headset" -> {
|
|
901
|
-
Log.d(TAG, "Setting audio route to Headset (wired).")
|
|
902
850
|
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
903
851
|
}
|
|
904
852
|
else -> {
|
|
905
|
-
Log.w(TAG, "Unknown audio route: $route
|
|
853
|
+
Log.w(TAG, "Unknown audio route: $route")
|
|
906
854
|
return
|
|
907
855
|
}
|
|
908
856
|
}
|
|
@@ -932,34 +880,31 @@ object CallEngine {
|
|
|
932
880
|
else -> "Earpiece"
|
|
933
881
|
}
|
|
934
882
|
|
|
935
|
-
Log.d(TAG, "Setting initial audio route
|
|
883
|
+
Log.d(TAG, "Setting initial audio route: $defaultRoute")
|
|
936
884
|
setAudioRoute(defaultRoute)
|
|
937
885
|
}
|
|
938
886
|
|
|
887
|
+
private fun setAudioMode() {
|
|
888
|
+
audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION
|
|
889
|
+
}
|
|
890
|
+
|
|
939
891
|
private fun resetAudioMode() {
|
|
940
|
-
val context = requireContext()
|
|
941
|
-
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
942
892
|
if (activeCalls.isEmpty()) {
|
|
943
|
-
Log.d(TAG, "Resetting audio mode to NORMAL as no active calls remain.")
|
|
944
893
|
audioManager?.mode = AudioManager.MODE_NORMAL
|
|
945
894
|
audioManager?.stopBluetoothSco()
|
|
946
895
|
audioManager?.isBluetoothScoOn = false
|
|
947
896
|
audioManager?.isSpeakerphoneOn = false
|
|
948
897
|
abandonAudioFocus()
|
|
949
|
-
} else {
|
|
950
|
-
Log.d(TAG, "Audio mode not reset; ${activeCalls.size} calls still active.")
|
|
951
898
|
}
|
|
952
899
|
}
|
|
953
900
|
|
|
954
901
|
// --- Audio Device Callback ---
|
|
955
902
|
private val audioDeviceCallback = object : AudioDeviceCallback() {
|
|
956
903
|
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
|
|
957
|
-
Log.d(TAG, "Audio devices added. Checking for changes.")
|
|
958
904
|
emitAudioDevicesChangedIfNeeded()
|
|
959
905
|
}
|
|
960
906
|
|
|
961
907
|
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
|
|
962
|
-
Log.d(TAG, "Audio devices removed. Checking for changes.")
|
|
963
908
|
emitAudioDevicesChangedIfNeeded()
|
|
964
909
|
}
|
|
965
910
|
}
|
|
@@ -968,14 +913,12 @@ object CallEngine {
|
|
|
968
913
|
val context = requireContext()
|
|
969
914
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
970
915
|
audioManager?.registerAudioDeviceCallback(audioDeviceCallback, null)
|
|
971
|
-
Log.d(TAG, "Audio device callback registered.")
|
|
972
916
|
}
|
|
973
917
|
|
|
974
918
|
fun unregisterAudioDeviceCallback() {
|
|
975
919
|
val context = requireContext()
|
|
976
920
|
audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
977
921
|
audioManager?.unregisterAudioDeviceCallback(audioDeviceCallback)
|
|
978
|
-
Log.d(TAG, "Audio device callback unregistered.")
|
|
979
922
|
}
|
|
980
923
|
|
|
981
924
|
private fun emitAudioDevicesChangedIfNeeded() {
|
|
@@ -1004,14 +947,14 @@ object CallEngine {
|
|
|
1004
947
|
PowerManager.SCREEN_DIM_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
|
1005
948
|
"CallEngine:WakeLock"
|
|
1006
949
|
)
|
|
1007
|
-
wakeLock?.acquire(10 * 60 * 1000L
|
|
1008
|
-
Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK
|
|
950
|
+
wakeLock?.acquire(10 * 60 * 1000L)
|
|
951
|
+
Log.d(TAG, "Acquired SCREEN_DIM_WAKE_LOCK")
|
|
1009
952
|
}
|
|
1010
953
|
} else {
|
|
1011
954
|
wakeLock?.let {
|
|
1012
955
|
if (it.isHeld) {
|
|
1013
956
|
it.release()
|
|
1014
|
-
Log.d(TAG, "Released SCREEN_DIM_WAKE_LOCK
|
|
957
|
+
Log.d(TAG, "Released SCREEN_DIM_WAKE_LOCK")
|
|
1015
958
|
}
|
|
1016
959
|
}
|
|
1017
960
|
wakeLock = null
|
|
@@ -1035,17 +978,7 @@ object CallEngine {
|
|
|
1035
978
|
}
|
|
1036
979
|
|
|
1037
980
|
private fun rejectIncomingCallCollision(callId: String, reason: String) {
|
|
1038
|
-
// Remove metadata for rejected call
|
|
1039
981
|
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
982
|
emitEvent(CallEventType.CALL_REJECTED, JSONObject().apply {
|
|
1050
983
|
put("callId", callId)
|
|
1051
984
|
put("reason", reason)
|
|
@@ -1081,13 +1014,12 @@ object CallEngine {
|
|
|
1081
1014
|
|
|
1082
1015
|
val manager = context.getSystemService(NotificationManager::class.java)
|
|
1083
1016
|
manager.createNotificationChannel(channel)
|
|
1084
|
-
Log.d(TAG, "Notification channel '$NOTIF_CHANNEL_ID' created/updated.")
|
|
1085
1017
|
}
|
|
1086
1018
|
}
|
|
1087
1019
|
|
|
1088
1020
|
private fun showIncomingCallUI(callId: String, callerName: String, callType: String) {
|
|
1089
1021
|
val context = requireContext()
|
|
1090
|
-
Log.d(TAG, "Showing incoming call UI for $callId
|
|
1022
|
+
Log.d(TAG, "Showing incoming call UI for $callId")
|
|
1091
1023
|
createNotificationChannel()
|
|
1092
1024
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
1093
1025
|
|
|
@@ -1153,10 +1085,8 @@ object CallEngine {
|
|
|
1153
1085
|
setInitialAudioRoute(callType)
|
|
1154
1086
|
}
|
|
1155
1087
|
|
|
1156
|
-
// Made public for CallActivity
|
|
1157
1088
|
fun cancelIncomingCallUI() {
|
|
1158
1089
|
val context = requireContext()
|
|
1159
|
-
Log.d(TAG, "Cancelling incoming call UI.")
|
|
1160
1090
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
1161
1091
|
notificationManager.cancel(NOTIF_ID)
|
|
1162
1092
|
stopRingtone()
|
|
@@ -1165,21 +1095,17 @@ object CallEngine {
|
|
|
1165
1095
|
// --- Service Management ---
|
|
1166
1096
|
private fun startForegroundService() {
|
|
1167
1097
|
val context = requireContext()
|
|
1168
|
-
Log.d(TAG, "Starting CallForegroundService.")
|
|
1169
|
-
|
|
1170
1098
|
val currentCall = activeCalls.values.find {
|
|
1171
1099
|
it.state == CallState.ACTIVE || it.state == CallState.INCOMING ||
|
|
1172
1100
|
it.state == CallState.DIALING || it.state == CallState.HELD
|
|
1173
1101
|
}
|
|
1174
1102
|
|
|
1175
1103
|
val intent = Intent(context, CallForegroundService::class.java)
|
|
1176
|
-
|
|
1177
1104
|
if (currentCall != null) {
|
|
1178
1105
|
intent.putExtra("callId", currentCall.callId)
|
|
1179
1106
|
intent.putExtra("callType", currentCall.callType)
|
|
1180
1107
|
intent.putExtra("displayName", currentCall.displayName)
|
|
1181
1108
|
intent.putExtra("state", currentCall.state.name)
|
|
1182
|
-
Log.d(TAG, "Starting foreground service with call info: ${currentCall.callId}")
|
|
1183
1109
|
}
|
|
1184
1110
|
|
|
1185
1111
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
@@ -1191,11 +1117,14 @@ object CallEngine {
|
|
|
1191
1117
|
|
|
1192
1118
|
private fun stopForegroundService() {
|
|
1193
1119
|
val context = requireContext()
|
|
1194
|
-
Log.d(TAG, "Stopping CallForegroundService.")
|
|
1195
1120
|
val intent = Intent(context, CallForegroundService::class.java)
|
|
1196
1121
|
context.stopService(intent)
|
|
1197
1122
|
}
|
|
1198
1123
|
|
|
1124
|
+
private fun updateForegroundNotification() {
|
|
1125
|
+
startForegroundService() // Just restart the service with updated info
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1199
1128
|
private fun bringAppToForeground() {
|
|
1200
1129
|
val context = requireContext()
|
|
1201
1130
|
val packageName = context.packageName
|
|
@@ -1204,11 +1133,6 @@ object CallEngine {
|
|
|
1204
1133
|
|
|
1205
1134
|
if (isCallActive()) {
|
|
1206
1135
|
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
1136
|
}
|
|
1213
1137
|
|
|
1214
1138
|
try {
|
|
@@ -1234,14 +1158,10 @@ object CallEngine {
|
|
|
1234
1158
|
|
|
1235
1159
|
try {
|
|
1236
1160
|
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)
|
|
1161
|
+
Log.d(TAG, "PhoneAccount registered successfully")
|
|
1240
1162
|
} catch (e: Exception) {
|
|
1241
1163
|
Log.e(TAG, "Failed to register PhoneAccount: ${e.message}", e)
|
|
1242
1164
|
}
|
|
1243
|
-
} else {
|
|
1244
|
-
Log.d(TAG, "PhoneAccount already registered.")
|
|
1245
1165
|
}
|
|
1246
1166
|
}
|
|
1247
1167
|
|
|
@@ -1257,115 +1177,65 @@ object CallEngine {
|
|
|
1257
1177
|
private fun playRingtone() {
|
|
1258
1178
|
val context = requireContext()
|
|
1259
1179
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
1260
|
-
|
|
1261
|
-
return
|
|
1180
|
+
return // System handles it
|
|
1262
1181
|
}
|
|
1263
1182
|
|
|
1264
1183
|
try {
|
|
1265
|
-
Log.d(TAG, "Playing ringtone (for Android < S).")
|
|
1266
1184
|
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
1267
1185
|
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
1186
|
ringtone?.play()
|
|
1273
1187
|
} catch (e: Exception) {
|
|
1274
|
-
Log.e(TAG, "Failed to play ringtone: ${e.message}"
|
|
1188
|
+
Log.e(TAG, "Failed to play ringtone: ${e.message}")
|
|
1275
1189
|
}
|
|
1276
1190
|
}
|
|
1277
1191
|
|
|
1278
|
-
// Made public for CallActivity and CallManager
|
|
1279
1192
|
fun stopRingtone() {
|
|
1280
1193
|
try {
|
|
1281
|
-
|
|
1282
|
-
ringtone?.stop()
|
|
1283
|
-
Log.d(TAG, "Ringtone stopped.")
|
|
1284
|
-
}
|
|
1194
|
+
ringtone?.stop()
|
|
1285
1195
|
} catch (e: Exception) {
|
|
1286
|
-
Log.e(TAG, "Error stopping ringtone: ${e.message}"
|
|
1196
|
+
Log.e(TAG, "Error stopping ringtone: ${e.message}")
|
|
1287
1197
|
}
|
|
1288
1198
|
ringtone = null
|
|
1289
1199
|
}
|
|
1290
1200
|
|
|
1291
1201
|
private fun startRingback() {
|
|
1292
1202
|
val context = requireContext()
|
|
1293
|
-
if (ringbackPlayer?.isPlaying == true)
|
|
1294
|
-
Log.d(TAG, "Ringback tone already playing.")
|
|
1295
|
-
return
|
|
1296
|
-
}
|
|
1203
|
+
if (ringbackPlayer?.isPlaying == true) return
|
|
1297
1204
|
|
|
1298
1205
|
try {
|
|
1299
1206
|
val ringbackUri = Uri.parse("android.resource://${context.packageName}/raw/ringback_tone")
|
|
1300
1207
|
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
1208
|
ringbackPlayer?.apply {
|
|
1307
1209
|
isLooping = true
|
|
1308
|
-
setAudioAttributes(
|
|
1309
|
-
AudioAttributes.Builder()
|
|
1310
|
-
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
|
|
1311
|
-
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
1312
|
-
.build()
|
|
1313
|
-
)
|
|
1314
1210
|
start()
|
|
1315
|
-
Log.d(TAG, "Ringback tone started.")
|
|
1316
1211
|
}
|
|
1317
1212
|
} catch (e: Exception) {
|
|
1318
|
-
Log.e(TAG, "Failed to play ringback tone: ${e.message}"
|
|
1213
|
+
Log.e(TAG, "Failed to play ringback tone: ${e.message}")
|
|
1319
1214
|
}
|
|
1320
1215
|
}
|
|
1321
1216
|
|
|
1322
1217
|
private fun stopRingback() {
|
|
1323
1218
|
try {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
ringbackPlayer?.release()
|
|
1327
|
-
Log.d(TAG, "Ringback tone stopped and released.")
|
|
1328
|
-
}
|
|
1219
|
+
ringbackPlayer?.stop()
|
|
1220
|
+
ringbackPlayer?.release()
|
|
1329
1221
|
} catch (e: Exception) {
|
|
1330
|
-
Log.e(TAG, "Error stopping ringback
|
|
1222
|
+
Log.e(TAG, "Error stopping ringback: ${e.message}")
|
|
1331
1223
|
} finally {
|
|
1332
1224
|
ringbackPlayer = null
|
|
1333
1225
|
}
|
|
1334
1226
|
}
|
|
1335
1227
|
|
|
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")
|
|
1228
|
+
// --- Cleanup ---
|
|
1229
|
+
private fun cleanup() {
|
|
1230
|
+
Log.d(TAG, "Performing cleanup")
|
|
1360
1231
|
stopForegroundService()
|
|
1361
1232
|
keepScreenAwake(false)
|
|
1362
1233
|
resetAudioMode()
|
|
1363
|
-
isSystemCallActive = false
|
|
1364
1234
|
}
|
|
1365
1235
|
|
|
1366
1236
|
// --- Lifecycle Management ---
|
|
1367
1237
|
fun onApplicationTerminate() {
|
|
1368
|
-
Log.d(TAG, "Application terminating
|
|
1238
|
+
Log.d(TAG, "Application terminating")
|
|
1369
1239
|
|
|
1370
1240
|
// End all calls properly
|
|
1371
1241
|
activeCalls.keys.toList().forEach { callId ->
|
|
@@ -1380,8 +1250,7 @@ object CallEngine {
|
|
|
1380
1250
|
callMetadata.clear()
|
|
1381
1251
|
currentCallId = null
|
|
1382
1252
|
|
|
1383
|
-
|
|
1384
|
-
finalCleanup()
|
|
1253
|
+
cleanup()
|
|
1385
1254
|
|
|
1386
1255
|
// Clear callbacks
|
|
1387
1256
|
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
|
}
|