@qusaieilouti99/call-manager 0.1.44 → 0.1.46
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/AndroidManifest.xml +55 -10
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/CallActivity.kt +7 -4
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/CallEngine.kt +93 -86
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/CallNotificationActionReceiver.kt +12 -4
- package/android/src/main/java/com/margelo/nitro/qusaieilouti99/callmanager/MyConnection.kt +5 -7
- package/package.json +1 -1
|
@@ -1,22 +1,67 @@
|
|
|
1
|
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
-
package="com.margelo.nitro.qusaieilouti99.callmanager">
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
2
|
|
|
4
|
-
<!--
|
|
3
|
+
<!-- REQUIRED FOR TELECOM API (Self-managed connections) -->
|
|
5
4
|
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
|
6
|
-
|
|
5
|
+
<!-- Required for recording audio in calls -->
|
|
7
6
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
7
|
+
<!-- Required for changing audio routes like speakerphone -->
|
|
8
8
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
|
9
|
+
<!-- For keeping screen awake during calls -->
|
|
9
10
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
11
|
+
<!-- For vibrating the device (e.g., incoming call) -->
|
|
10
12
|
<uses-permission android:name="android.permission.VIBRATE" />
|
|
11
|
-
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
|
12
|
-
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
13
|
-
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
14
|
-
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
|
15
13
|
|
|
16
|
-
<!-- Bluetooth permissions -->
|
|
14
|
+
<!-- Bluetooth permissions for audio routing -->
|
|
15
|
+
<!-- For Android 11 (API 30) and lower -->
|
|
17
16
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
|
18
17
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
|
18
|
+
<!-- For Android 12 (API 31) and higher -->
|
|
19
19
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
20
20
|
|
|
21
|
-
<!--
|
|
21
|
+
<!-- Required for foreground service on older devices (< Android P) -->
|
|
22
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
23
|
+
<!-- Required for foreground service with phoneCall type on Android 12 (API 31) and higher -->
|
|
24
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
|
25
|
+
|
|
26
|
+
<application>
|
|
27
|
+
<!-- Your CallActivity for full-screen incoming call UI -->
|
|
28
|
+
<activity
|
|
29
|
+
android:name=".CallActivity"
|
|
30
|
+
android:exported="true"
|
|
31
|
+
android:showOnLockScreen="true"
|
|
32
|
+
android:turnScreenOn="true"
|
|
33
|
+
android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
|
|
34
|
+
android:screenOrientation="portrait"
|
|
35
|
+
android:launchMode="singleTop"
|
|
36
|
+
android:excludeFromRecents="true"
|
|
37
|
+
android:taskAffinity=""
|
|
38
|
+
android:windowSoftInputMode="adjustResize">
|
|
39
|
+
<intent-filter>
|
|
40
|
+
<action android:name="android.intent.action.VIEW"/>
|
|
41
|
+
<category android:name="android.intent.category.DEFAULT"/>
|
|
42
|
+
</intent-filter>
|
|
43
|
+
</activity>
|
|
44
|
+
|
|
45
|
+
<!-- The ConnectionService, crucial for Telecom API integration -->
|
|
46
|
+
<service
|
|
47
|
+
android:name=".MyConnectionService"
|
|
48
|
+
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
|
49
|
+
android:exported="true">
|
|
50
|
+
<intent-filter>
|
|
51
|
+
<action android:name="android.telecom.ConnectionService" />
|
|
52
|
+
</intent-filter>
|
|
53
|
+
</service>
|
|
54
|
+
|
|
55
|
+
<!-- The foreground service to keep the call alive in the background -->
|
|
56
|
+
<service
|
|
57
|
+
android:name=".CallForegroundService"
|
|
58
|
+
android:enabled="true"
|
|
59
|
+
android:exported="false"
|
|
60
|
+
android:foregroundServiceType="phoneCall" />
|
|
61
|
+
|
|
62
|
+
<!-- Receiver for notification actions (Answer/Decline) -->
|
|
63
|
+
<receiver
|
|
64
|
+
android:name=".CallNotificationActionReceiver"
|
|
65
|
+
android:exported="false" />
|
|
66
|
+
</application>
|
|
22
67
|
</manifest>
|
|
@@ -49,8 +49,8 @@ class CallActivity : Activity() {
|
|
|
49
49
|
callType = intent.getStringExtra("callType") ?: "Audio"
|
|
50
50
|
Log.d(TAG, "CallActivity received callId: $callId, callType: $callType")
|
|
51
51
|
|
|
52
|
-
//
|
|
53
|
-
|
|
52
|
+
// FIXED: Immediate cleanup of notifications when CallActivity is shown
|
|
53
|
+
CallEngine.cancelIncomingCallUI(this)
|
|
54
54
|
|
|
55
55
|
val callerName = intent.getStringExtra("callerName") ?: "Unknown"
|
|
56
56
|
val nameView = findViewById<TextView>(R.id.caller_name)
|
|
@@ -63,7 +63,7 @@ class CallActivity : Activity() {
|
|
|
63
63
|
Log.d(TAG, "CallActivity: Answer button clicked for callId: $callId")
|
|
64
64
|
finishReason = FinishReason.ANSWER
|
|
65
65
|
|
|
66
|
-
// Use
|
|
66
|
+
// FIXED: Use single source of truth - this will handle all cleanup
|
|
67
67
|
CallEngine.answerCall(this, callId)
|
|
68
68
|
finishCallActivity()
|
|
69
69
|
}
|
|
@@ -82,7 +82,10 @@ class CallActivity : Activity() {
|
|
|
82
82
|
super.onDestroy()
|
|
83
83
|
Log.d(TAG, "CallActivity onDestroy for callId: $callId. Reason: $finishReason")
|
|
84
84
|
timeoutHandler.removeCallbacks(timeoutRunnable)
|
|
85
|
-
|
|
85
|
+
|
|
86
|
+
// FIXED: Ensure cleanup happens regardless of how activity ends
|
|
87
|
+
CallEngine.stopRingtone()
|
|
88
|
+
CallEngine.cancelIncomingCallUI(this)
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
override fun onBackPressed() {
|
|
@@ -28,7 +28,6 @@ import org.json.JSONArray
|
|
|
28
28
|
import org.json.JSONObject
|
|
29
29
|
import java.util.UUID
|
|
30
30
|
import java.util.concurrent.ConcurrentHashMap
|
|
31
|
-
import android.view.WindowManager
|
|
32
31
|
|
|
33
32
|
object CallEngine {
|
|
34
33
|
private const val TAG = "CallEngine"
|
|
@@ -106,7 +105,6 @@ object CallEngine {
|
|
|
106
105
|
}
|
|
107
106
|
|
|
108
107
|
// --- Public API ---
|
|
109
|
-
|
|
110
108
|
fun setCanMakeMultipleCalls(allow: Boolean) {
|
|
111
109
|
canMakeMultipleCalls = allow
|
|
112
110
|
Log.d(TAG, "canMakeMultipleCalls set to: $allow")
|
|
@@ -165,6 +163,7 @@ object CallEngine {
|
|
|
165
163
|
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
|
|
166
164
|
startForegroundService(context)
|
|
167
165
|
Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
|
|
166
|
+
// REMOVED: Don't bring app to foreground for incoming calls - only when answered
|
|
168
167
|
} catch (e: SecurityException) {
|
|
169
168
|
Log.e(TAG, "SecurityException: Failed to report incoming call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
|
|
170
169
|
endCall(context, callId)
|
|
@@ -218,7 +217,8 @@ object CallEngine {
|
|
|
218
217
|
startForegroundService(context)
|
|
219
218
|
Log.d(TAG, "Successfully reported outgoing call to TelecomManager via placeCall for $callId")
|
|
220
219
|
startRingback()
|
|
221
|
-
//
|
|
220
|
+
// CHANGED: Bring app to foreground for outgoing calls
|
|
221
|
+
bringAppToForeground(context)
|
|
222
222
|
keepScreenAwake(context, true)
|
|
223
223
|
if (parsedCallType == "Video") {
|
|
224
224
|
setAudioRoute(context, "Speaker")
|
|
@@ -235,11 +235,23 @@ object CallEngine {
|
|
|
235
235
|
notifyCallStateChanged(context)
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
Log.d(TAG, "
|
|
238
|
+
// SINGLE SOURCE OF TRUTH: Core function for handling when remote party answers
|
|
239
|
+
fun callAnsweredFromJS(context: Context, callId: String) {
|
|
240
|
+
Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
|
|
241
|
+
coreCallAnswered(context, callId, isLocalAnswer = false)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// SINGLE SOURCE OF TRUTH: Core function for handling local answer (user answering)
|
|
245
|
+
fun answerCall(context: Context, callId: String) {
|
|
246
|
+
Log.d(TAG, "answerCall: $callId - local party answering")
|
|
247
|
+
coreCallAnswered(context, callId, isLocalAnswer = true)
|
|
248
|
+
}
|
|
241
249
|
|
|
242
|
-
|
|
250
|
+
// SINGLE SOURCE OF TRUTH: Core function that handles ALL call answering logic
|
|
251
|
+
private fun coreCallAnswered(context: Context, callId: String, isLocalAnswer: Boolean) {
|
|
252
|
+
Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
|
|
253
|
+
|
|
254
|
+
// Stop all ringtones and notifications immediately
|
|
243
255
|
stopRingtone()
|
|
244
256
|
stopRingback()
|
|
245
257
|
cancelIncomingCallUI(context)
|
|
@@ -248,41 +260,21 @@ object CallEngine {
|
|
|
248
260
|
activeCalls[callId]?.state = CallState.ACTIVE
|
|
249
261
|
currentCallId = callId
|
|
250
262
|
|
|
251
|
-
// Handle multi-call scenario
|
|
252
263
|
if (!canMakeMultipleCalls) {
|
|
253
264
|
activeCalls.filter { it.key != callId }.values.forEach { it.state = CallState.HELD }
|
|
254
265
|
}
|
|
255
266
|
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// Start foreground service if not already started
|
|
267
|
+
// Bring app to foreground only when call is answered
|
|
268
|
+
bringAppToForeground(context)
|
|
260
269
|
startForegroundService(context)
|
|
261
|
-
|
|
262
|
-
// Wake up screen and clear lock screen if needed
|
|
263
270
|
keepScreenAwake(context, true)
|
|
264
|
-
|
|
265
|
-
// Bring app to foreground NOW (not earlier)
|
|
266
|
-
bringAppToForeground(context)
|
|
267
|
-
|
|
268
|
-
// Reset audio mode
|
|
269
271
|
resetAudioMode(context)
|
|
270
272
|
|
|
271
|
-
//
|
|
273
|
+
// Emit event
|
|
274
|
+
emitEvent(CallEventType.CALL_ANSWERED, JSONObject().put("callId", callId))
|
|
272
275
|
notifyCallStateChanged(context)
|
|
273
276
|
|
|
274
|
-
Log.d(TAG, "
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Public answer methods that delegate to the core function
|
|
278
|
-
fun callAnsweredFromJS(context: Context, callId: String) {
|
|
279
|
-
Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
|
|
280
|
-
coreAnswerCall(context, callId, false)
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
fun answerCall(context: Context, callId: String) {
|
|
284
|
-
Log.d(TAG, "answerCall: $callId - local party answering")
|
|
285
|
-
coreAnswerCall(context, callId, true)
|
|
277
|
+
Log.d(TAG, "Call $callId successfully answered and UI cleaned up")
|
|
286
278
|
}
|
|
287
279
|
|
|
288
280
|
fun holdCall(context: Context, callId: String) {
|
|
@@ -316,103 +308,113 @@ object CallEngine {
|
|
|
316
308
|
emitEvent(CallEventType.CALL_UNMUTED, JSONObject().put("callId", callId))
|
|
317
309
|
}
|
|
318
310
|
|
|
311
|
+
// SINGLE SOURCE OF TRUTH: Core function that handles ALL call ending logic
|
|
319
312
|
fun endCall(context: Context, callId: String) {
|
|
320
313
|
appContext = context.applicationContext
|
|
321
314
|
Log.d(TAG, "endCall: $callId")
|
|
315
|
+
|
|
316
|
+
// Core cleanup logic
|
|
317
|
+
coreEndCall(context, callId)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
fun endAllCalls(context: Context) {
|
|
321
|
+
Log.d(TAG, "endAllCalls: Ending all active calls.")
|
|
322
|
+
if (activeCalls.isEmpty()) {
|
|
323
|
+
Log.d(TAG, "No active calls, nothing to do.")
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
activeCalls.keys.toList().forEach { callId ->
|
|
327
|
+
coreEndCall(context, callId)
|
|
328
|
+
}
|
|
329
|
+
activeCalls.clear()
|
|
330
|
+
telecomConnections.clear()
|
|
331
|
+
currentCallId = null
|
|
332
|
+
|
|
333
|
+
// Final cleanup
|
|
334
|
+
finalCleanup(context)
|
|
335
|
+
notifyCallStateChanged(context)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// SINGLE SOURCE OF TRUTH: Core function that handles ending a single call
|
|
339
|
+
private fun coreEndCall(context: Context, callId: String) {
|
|
340
|
+
Log.d(TAG, "coreEndCall: $callId")
|
|
341
|
+
|
|
342
|
+
// Update call state
|
|
322
343
|
activeCalls[callId]?.state = CallState.ENDED
|
|
323
344
|
activeCalls.remove(callId)
|
|
324
345
|
Log.d(TAG, "Call $callId removed from activeCalls. Remaining: ${activeCalls.size}")
|
|
325
346
|
|
|
347
|
+
// Stop ringtones and notifications
|
|
326
348
|
stopRingback()
|
|
327
349
|
stopRingtone()
|
|
350
|
+
cancelIncomingCallUI(context)
|
|
328
351
|
|
|
352
|
+
// Update current call
|
|
329
353
|
if (currentCallId == callId) {
|
|
330
354
|
currentCallId = activeCalls.filter { it.value.state != CallState.ENDED }.keys.firstOrNull()
|
|
331
355
|
Log.d(TAG, "Current call was $callId. New currentCallId: $currentCallId")
|
|
332
356
|
}
|
|
333
357
|
|
|
334
|
-
|
|
335
|
-
|
|
358
|
+
// Handle telecom connection
|
|
336
359
|
val connection = telecomConnections[callId]
|
|
337
360
|
if (connection != null) {
|
|
338
361
|
connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
339
362
|
connection.destroy()
|
|
340
363
|
removeTelecomConnection(callId)
|
|
341
364
|
Log.d(TAG, "Telecom Connection for $callId disconnected and destroyed.")
|
|
342
|
-
} else {
|
|
343
|
-
Log.d(TAG, "No Telecom Connection found for callId=$callId. Likely an outgoing call not initially handled by Telecom.")
|
|
344
365
|
}
|
|
345
366
|
|
|
367
|
+
// If no more calls, do final cleanup
|
|
346
368
|
if (activeCalls.isEmpty()) {
|
|
347
|
-
|
|
348
|
-
stopForegroundService(context)
|
|
349
|
-
keepScreenAwake(context, false)
|
|
350
|
-
resetAudioMode(context)
|
|
351
|
-
clearLockScreenBypass(context) // Clear lock screen bypass when all calls end
|
|
352
|
-
} else {
|
|
353
|
-
Log.d(TAG, "Other calls still active. Foreground service remains.")
|
|
369
|
+
finalCleanup(context)
|
|
354
370
|
}
|
|
355
371
|
|
|
356
372
|
emitEvent(CallEventType.CALL_ENDED, JSONObject().put("callId", callId))
|
|
357
373
|
notifyCallStateChanged(context)
|
|
358
374
|
}
|
|
359
375
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
return
|
|
365
|
-
}
|
|
366
|
-
activeCalls.keys.toList().forEach { callId ->
|
|
367
|
-
endCall(context, callId)
|
|
368
|
-
}
|
|
369
|
-
activeCalls.clear()
|
|
370
|
-
telecomConnections.clear()
|
|
371
|
-
currentCallId = null
|
|
372
|
-
cancelIncomingCallUI(context)
|
|
376
|
+
// SINGLE SOURCE OF TRUTH: Final cleanup when all calls are ended
|
|
377
|
+
private fun finalCleanup(context: Context) {
|
|
378
|
+
Log.d(TAG, "Performing final cleanup - no active calls remaining")
|
|
379
|
+
|
|
373
380
|
stopForegroundService(context)
|
|
374
|
-
stopRingtone()
|
|
375
|
-
stopRingback()
|
|
376
381
|
keepScreenAwake(context, false)
|
|
377
382
|
resetAudioMode(context)
|
|
378
|
-
clearLockScreenBypass(context) // Clear lock screen bypass
|
|
379
|
-
notifyCallStateChanged(context)
|
|
380
|
-
}
|
|
381
383
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
384
|
+
// FIXED: Clear lock screen bypass when calls end
|
|
385
|
+
clearLockScreenBypass(context)
|
|
386
|
+
}
|
|
385
387
|
|
|
386
|
-
//
|
|
388
|
+
// NEW: Function to clear lock screen bypass
|
|
387
389
|
private fun clearLockScreenBypass(context: Context) {
|
|
388
|
-
Log.d(TAG, "Clearing lock screen bypass for all activities.")
|
|
389
|
-
|
|
390
|
-
// Send broadcast to MainActivity to clear flags if it's active
|
|
391
|
-
val intent = Intent("com.pingme2022.CLEAR_LOCK_SCREEN_BYPASS")
|
|
392
|
-
context.sendBroadcast(intent)
|
|
393
|
-
|
|
394
|
-
// Also try to clear on the app context activities
|
|
395
390
|
try {
|
|
396
391
|
if (context is Activity) {
|
|
397
392
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
|
398
|
-
|
|
399
|
-
|
|
393
|
+
context.setShowWhenLocked(false)
|
|
394
|
+
context.setTurnScreenOn(false)
|
|
400
395
|
} else {
|
|
401
396
|
context.window.clearFlags(
|
|
402
|
-
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
|
403
|
-
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
|
404
|
-
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
|
397
|
+
android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
|
398
|
+
android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
|
399
|
+
android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
|
405
400
|
)
|
|
406
401
|
}
|
|
407
|
-
Log.d(TAG, "
|
|
402
|
+
Log.d(TAG, "Lock screen bypass flags cleared for Activity")
|
|
408
403
|
}
|
|
409
404
|
} catch (e: Exception) {
|
|
410
|
-
Log.w(TAG, "Could not clear lock screen bypass
|
|
405
|
+
Log.w(TAG, "Could not clear lock screen bypass flags: ${e.message}")
|
|
411
406
|
}
|
|
412
407
|
}
|
|
413
408
|
|
|
414
|
-
|
|
409
|
+
fun getActiveCalls(): List<CallInfo> = activeCalls.values.toList()
|
|
410
|
+
fun getCurrentCallId(): String? = currentCallId
|
|
411
|
+
fun isCallActive(): Boolean = activeCalls.any {
|
|
412
|
+
it.value.state == CallState.ACTIVE ||
|
|
413
|
+
it.value.state == CallState.INCOMING ||
|
|
414
|
+
it.value.state == CallState.DIALING
|
|
415
|
+
}
|
|
415
416
|
|
|
417
|
+
// Enhanced incoming call UI with better cleanup
|
|
416
418
|
fun showIncomingCallUI(context: Context, callId: String, callerName: String, callType: String) {
|
|
417
419
|
Log.d(TAG, "Showing incoming call UI for $callId, caller: $callerName, callType: $callType")
|
|
418
420
|
createNotificationChannel(context)
|
|
@@ -494,17 +496,20 @@ object CallEngine {
|
|
|
494
496
|
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
|
495
497
|
launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
496
498
|
|
|
499
|
+
// Handle lock screen bypass for active calls
|
|
497
500
|
if (isCallActive()) {
|
|
498
|
-
|
|
501
|
+
// Only set lock screen bypass when bringing to foreground during active calls
|
|
502
|
+
launchIntent?.putExtra("BYPASS_LOCK_SCREEN", true)
|
|
503
|
+
Log.d(TAG, "App brought to foreground with lock screen bypass for active call")
|
|
499
504
|
} else {
|
|
500
|
-
Log.d(TAG, "App brought to foreground
|
|
505
|
+
Log.d(TAG, "App brought to foreground without lock screen bypass")
|
|
501
506
|
}
|
|
502
507
|
|
|
503
508
|
context.startActivity(launchIntent)
|
|
504
509
|
}
|
|
505
510
|
|
|
506
511
|
// Rest of the methods remain the same...
|
|
507
|
-
//
|
|
512
|
+
// (getAudioDevices, setAudioRoute, keepScreenAwake, etc. - keeping them unchanged for brevity)
|
|
508
513
|
|
|
509
514
|
// --- Audio Device Management ---
|
|
510
515
|
fun getAudioDevices(): AudioRoutesInfo {
|
|
@@ -640,9 +645,7 @@ object CallEngine {
|
|
|
640
645
|
}
|
|
641
646
|
}
|
|
642
647
|
|
|
643
|
-
//
|
|
644
|
-
// [Audio Device Change Listener, Call State Change Notification, etc.]
|
|
645
|
-
|
|
648
|
+
// --- Audio Device Change Listener ---
|
|
646
649
|
private val audioDeviceCallback = object : AudioDeviceCallback() {
|
|
647
650
|
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
|
|
648
651
|
Log.d(TAG, "Audio devices added. Emitting AUDIO_DEVICES_CHANGED.")
|
|
@@ -680,6 +683,7 @@ object CallEngine {
|
|
|
680
683
|
emitEvent(CallEventType.AUDIO_DEVICES_CHANGED, jsonPayload)
|
|
681
684
|
}
|
|
682
685
|
|
|
686
|
+
// --- Call State Change Notification ---
|
|
683
687
|
private fun notifyCallStateChanged(context: Context) {
|
|
684
688
|
val calls = getActiveCalls()
|
|
685
689
|
val jsonArray = JSONArray()
|
|
@@ -725,6 +729,7 @@ object CallEngine {
|
|
|
725
729
|
}
|
|
726
730
|
}
|
|
727
731
|
|
|
732
|
+
// PhoneAccount registration is used for both INCOMING and OUTGOING calls when interacting with Telecom
|
|
728
733
|
private fun registerPhoneAccount(context: Context) {
|
|
729
734
|
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
730
735
|
val phoneAccountHandle = getPhoneAccountHandle(context)
|
|
@@ -752,6 +757,7 @@ object CallEngine {
|
|
|
752
757
|
)
|
|
753
758
|
}
|
|
754
759
|
|
|
760
|
+
// --- Ringtone Management (for incoming calls, pre-Android S) ---
|
|
755
761
|
fun playRingtone(context: Context) {
|
|
756
762
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
757
763
|
Log.d(TAG, "playRingtone: Android S+ detected, system will handle ringtone via Telecom.")
|
|
@@ -783,6 +789,7 @@ object CallEngine {
|
|
|
783
789
|
ringtone = null
|
|
784
790
|
}
|
|
785
791
|
|
|
792
|
+
// --- Ringback Tone Management (for outgoing calls) ---
|
|
786
793
|
private fun startRingback() {
|
|
787
794
|
if (ringbackPlayer != null && ringbackPlayer!!.isPlaying) {
|
|
788
795
|
Log.d(TAG, "Ringback tone already playing.")
|
|
@@ -15,13 +15,21 @@ class CallNotificationActionReceiver : BroadcastReceiver() {
|
|
|
15
15
|
|
|
16
16
|
when (intent.action) {
|
|
17
17
|
"com.qusaieilouti99.callmanager.ANSWER_CALL" -> {
|
|
18
|
-
Log.d(TAG, "Answer action received for callId: $callId
|
|
18
|
+
Log.d(TAG, "Answer action received for callId: $callId")
|
|
19
19
|
|
|
20
|
-
// Use
|
|
21
|
-
CallEngine.
|
|
20
|
+
// FIXED: Use single source of truth for answering
|
|
21
|
+
val connection = CallEngine.getTelecomConnection(callId)
|
|
22
|
+
if (connection != null) {
|
|
23
|
+
connection.onAnswer() // This will trigger MyConnection.onAnswer()
|
|
24
|
+
Log.d(TAG, "Call answered via Telecom connection for callId: $callId")
|
|
25
|
+
} else {
|
|
26
|
+
Log.e(TAG, "No Telecom connection found for callId: $callId. Using direct answer.")
|
|
27
|
+
CallEngine.answerCall(context, callId)
|
|
28
|
+
}
|
|
29
|
+
// NOTE: Don't call bringAppToForeground here - it's handled in coreCallAnswered
|
|
22
30
|
}
|
|
23
31
|
"com.qusaieilouti99.callmanager.DECLINE_CALL" -> {
|
|
24
|
-
Log.d(TAG, "Decline action received for callId: $callId
|
|
32
|
+
Log.d(TAG, "Decline action received for callId: $callId")
|
|
25
33
|
CallEngine.endCall(context, callId)
|
|
26
34
|
}
|
|
27
35
|
else -> {
|
|
@@ -32,7 +32,6 @@ class MyConnection(
|
|
|
32
32
|
JSONObject(callDataJson).optString("callType", "Audio")
|
|
33
33
|
} catch (e: Exception) { "Audio" }
|
|
34
34
|
|
|
35
|
-
// Set connection properties and capabilities
|
|
36
35
|
connectionProperties = Connection.PROPERTY_SELF_MANAGED
|
|
37
36
|
connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or Connection.CAPABILITY_MUTE
|
|
38
37
|
|
|
@@ -52,7 +51,7 @@ class MyConnection(
|
|
|
52
51
|
Log.d(TAG, "Call answered via Telecom for callId: $callId")
|
|
53
52
|
setActive()
|
|
54
53
|
|
|
55
|
-
// Use
|
|
54
|
+
// FIXED: Use single source of truth for answering
|
|
56
55
|
CallEngine.answerCall(context, callId)
|
|
57
56
|
}
|
|
58
57
|
|
|
@@ -84,7 +83,7 @@ class MyConnection(
|
|
|
84
83
|
|
|
85
84
|
override fun onCallAudioStateChanged(state: CallAudioState) {
|
|
86
85
|
super.onCallAudioStateChanged(state)
|
|
87
|
-
Log.d(TAG, "Audio state changed for callId: $callId. muted=${state.isMuted}, route=${state.route}
|
|
86
|
+
Log.d(TAG, "Audio state changed for callId: $callId. muted=${state.isMuted}, route=${state.route}")
|
|
88
87
|
|
|
89
88
|
if (state.isMuted) {
|
|
90
89
|
CallEngine.muteCall(context, callId)
|
|
@@ -99,7 +98,6 @@ class MyConnection(
|
|
|
99
98
|
CallAudioState.ROUTE_WIRED_HEADSET -> "Headset"
|
|
100
99
|
else -> "Unknown"
|
|
101
100
|
}
|
|
102
|
-
Log.d(TAG, "CallAudioState changed, new route detected: $routeName")
|
|
103
101
|
CallEngine.emitEvent(
|
|
104
102
|
CallEventType.AUDIO_ROUTE_CHANGED,
|
|
105
103
|
JSONObject().put("callId", callId).put("route", routeName)
|
|
@@ -122,8 +120,8 @@ class MyConnection(
|
|
|
122
120
|
|
|
123
121
|
override fun onShowIncomingCallUi() {
|
|
124
122
|
super.onShowIncomingCallUi()
|
|
125
|
-
Log.d(TAG, "onShowIncomingCallUi for callId: $callId
|
|
126
|
-
// Don't bring
|
|
127
|
-
//
|
|
123
|
+
Log.d(TAG, "onShowIncomingCallUi for callId: $callId")
|
|
124
|
+
// REMOVED: Don't bring app to foreground for incoming calls
|
|
125
|
+
// CallEngine.bringAppToForeground(context)
|
|
128
126
|
}
|
|
129
127
|
}
|