Skip to content

Commit b90f850

Browse files
ai-swe-agentopenhands-agentOliver-Zimmerman
authored
WEBRTC-2550: Prevent socket disconnection when minimized and fix notification handling (#460)
* WEBRTC-2550: Prevent socket disconnection when app is minimized during active call * fix: disconnection on notification tap through onStop on MainActivity * chore: bump androidx_core * WEBRTC-2550: Implement CallNotification integration - Created CallNotificationService using NotificationCompat.CallStyle API - Created CallNotificationReceiver to handle notification actions - Updated MyFirebaseMessagingService to use the new CallNotificationService - Updated NotificationsService to support both legacy and modern notification styles - Updated AndroidManifest.xml files to register the new receiver * fix: import ComponentName and adjust notificationManager declaration * chore: update manifest to allow for full screen intent (required for call permissions) * WEBRTC-2550: Implement CallForegroundService to keep audio alive when minimized * feat: use correct callerName and callerNumber and handle call cancellation better * fix: catch better exceptions and remove magic number * chore: add service type as phone call * chore: add microphone permission * WEBRTC-2550: Fix socket disconnects when minimized during active calls - Added static flag in CallForegroundService to track if service is already running\n- Added method to check if service is running using both flag and system service check\n- Updated TelnyxViewModel to check if service is already running before starting it\n- Improved error handling in startForeground method to gracefully fall back to PHONE_CALL only if MICROPHONE permission fails\n- Added proper cleanup of service flag in onCreate, onStartCommand, and onDestroy * feat: startCallService before the app goes into the background * feat: catch specific exceptions and use singleTask to only open existing Activity * feat: catch non generic exceptions * WEBRTC-2550: Fix background activity launch blocked issue and make notification click accept call * Revert "WEBRTC-2550: Fix background activity launch blocked issue and make notification click accept call" This reverts commit 6875ade. * chore: add requiresAPI calls and adjust manifest for XML app * WEBRTC-2550: Fix background activity launch blocked issue and make notification click accept call * Revert "WEBRTC-2550: Fix background activity launch blocked issue and make notification click accept call" This reverts commit bf479af. * feat: use pending intent with activities instead of notification receiver * fix: rejection from push notification no longer starts call service * chore: bump up wait time for tests for slow connections * feat: more reliable form of call service start / stop (based on current call) * fix: set _handling push to false whenever a call ends - even if it wasn't true before just in case * fix: if device is locked, don't use full screen intent (as it attempts to launch activity) * feat: stop service when disconnecting if one is running * chore: clearer documentation on notification broadcast receiver usage * fix: ignore local candidates during negotiation, use shared mediaConstraints and adjust changelog for 1.5.1 release * fix: use IceTransportsType as NOHOST * chore: remove duplicate import logger --------- Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: Oliver Zimmerman <oezimmerman@gmail.com>
1 parent dd397d0 commit b90f850

File tree

25 files changed

+963
-171
lines changed

25 files changed

+963
-171
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,4 @@ connection_service_app/google-services.json
9292

9393
# system
9494
.DS_Store
95+
/platform-samples/

compose_app/src/androidTest/java/org/telnyx/webrtc/compose_app/MainActivityTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class MainActivityTest {
6262
composeTestRule.waitForIdle()
6363
composeTestRule.onNodeWithText(context.getString(R.string.connect)).performClick()
6464

65-
composeTestRule.waitUntil(10000) {
65+
composeTestRule.waitUntil(15000) {
6666
composeTestRule.onNodeWithText(context.getString(R.string.disconnect)).isDisplayed()
6767
}
6868

@@ -76,7 +76,7 @@ class MainActivityTest {
7676

7777
composeTestRule.waitForIdle()
7878

79-
composeTestRule.waitUntil(30000) {
79+
composeTestRule.waitUntil(40000) {
8080
composeTestRule.onNodeWithTag("callActiveView").isDisplayed()
8181
}
8282

compose_app/src/main/AndroidManifest.xml

+16
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
55
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
66
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL"/>
7+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
78
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
9+
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
10+
811

912
<application
1013
android:allowBackup="true"
@@ -17,6 +20,7 @@
1720
<activity
1821
android:name=".MainActivity"
1922
android:exported="true"
23+
android:launchMode="singleTask"
2024
android:theme="@style/Theme.TelnyxAndroidWebRTCSDK">
2125
<intent-filter>
2226
<action android:name="android.intent.action.MAIN" />
@@ -50,6 +54,18 @@
5054
android:name="activity_class_name"
5155
android:value="org.telnyx.webrtc.compose_app.MainActivity" />
5256
</service>
57+
58+
<service android:name="com.telnyx.webrtc.common.service.CallForegroundService"
59+
android:enabled="true"
60+
android:exported="true"
61+
android:foregroundServiceType="phoneCall|microphone"
62+
android:permission="android.permission.FOREGROUND_SERVICE_PHONE_CALL"
63+
android:process=":call_service" />
64+
65+
<receiver
66+
android:name="com.telnyx.webrtc.common.notification.CallNotificationReceiver"
67+
android:enabled="true"
68+
android:exported="false" />
5369
</application>
5470

5571
</manifest>

compose_app/src/main/java/org/telnyx/webrtc/compose_app/MainActivity.kt

+35-26
Original file line numberDiff line numberDiff line change
@@ -96,36 +96,45 @@ class MainActivity : ComponentActivity(), DefaultLifecycleObserver {
9696
}
9797

9898
private fun checkPermission() {
99+
// Create a mutable list of permissions that will always be needed.
100+
val permissions = mutableListOf(
101+
android.Manifest.permission.RECORD_AUDIO
102+
)
103+
104+
// Conditionally add permissions based on the API level.
99105
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
100-
Dexter.withContext(this)
101-
.withPermissions(
102-
android.Manifest.permission.POST_NOTIFICATIONS,
103-
android.Manifest.permission.RECORD_AUDIO
104-
)
105-
.withListener(object : MultiplePermissionsListener {
106-
override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
107-
report?.let {
108-
if (report.areAllPermissionsGranted()) {
109-
// All permissions are granted
110-
} else {
111-
// Some permissions are denied
112-
Toast.makeText(
113-
this@MainActivity,
114-
getString(R.string.notification_permission_text),
115-
Toast.LENGTH_LONG
116-
).show()
117-
}
106+
permissions.add(android.Manifest.permission.POST_NOTIFICATIONS)
107+
}
108+
if (Build.VERSION.SDK_INT >= 34) { // Only available on Android 14 and above.
109+
permissions.add(android.Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
110+
}
111+
112+
// Now use Dexter to check the permissions.
113+
Dexter.withContext(this)
114+
.withPermissions(*permissions.toTypedArray())
115+
.withListener(object : MultiplePermissionsListener {
116+
override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
117+
report?.let {
118+
if (report.areAllPermissionsGranted()) {
119+
// All permissions are granted.
120+
} else {
121+
// Some permissions are denied.
122+
Toast.makeText(
123+
this@MainActivity,
124+
getString(R.string.notification_permission_text),
125+
Toast.LENGTH_LONG
126+
).show()
118127
}
119128
}
129+
}
120130

121-
override fun onPermissionRationaleShouldBeShown(
122-
permissions: MutableList<PermissionRequest>?,
123-
token: PermissionToken?
124-
) {
125-
token?.continuePermissionRequest()
126-
}
127-
}).check()
128-
}
131+
override fun onPermissionRationaleShouldBeShown(
132+
permissions: MutableList<PermissionRequest>?,
133+
token: PermissionToken?
134+
) {
135+
token?.continuePermissionRequest()
136+
}
137+
}).check()
129138
}
130139
}
131140

compose_app/src/main/java/org/telnyx/webrtc/compose_app/ui/screens/HomeScreen.kt

+6-6
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ fun HomeScreen(navController: NavHostController, telnyxViewModel: TelnyxViewMode
109109
Scaffold(modifier = Modifier.padding(Dimens.mediumSpacing),
110110
topBar = {
111111
Column(
112-
verticalArrangement = Arrangement.spacedBy(Dimens.mediumSpacing),
112+
verticalArrangement = Arrangement.spacedBy(Dimens.smallSpacing),
113113
) {
114114
Spacer(modifier = Modifier.size(Dimens.mediumSpacing))
115115

@@ -162,10 +162,10 @@ fun HomeScreen(navController: NavHostController, telnyxViewModel: TelnyxViewMode
162162
bottom = it.calculateBottomPadding(),
163163
top = it.calculateTopPadding()
164164
),
165-
verticalArrangement = Arrangement.spacedBy(Dimens.mediumSpacing),
165+
verticalArrangement = Arrangement.spacedBy(Dimens.smallSpacing),
166166
) {
167167

168-
Spacer(modifier = Modifier.size(Dimens.mediumSpacing))
168+
Spacer(modifier = Modifier.size(Dimens.smallSpacing))
169169

170170
NavHost(navController = navController, startDestination = LoginScreenNav) {
171171
composable<LoginScreenNav> {
@@ -556,15 +556,15 @@ fun ProfileSwitcher(profileName: String, onProfileSwitch: () -> Unit = {}) {
556556

557557
@Composable
558558
fun SessionItem(sessionId: String) {
559-
Column(verticalArrangement = Arrangement.spacedBy(Dimens.extraSmallSpacing)) {
559+
Column(verticalArrangement = Arrangement.spacedBy(Dimens.spacing4dp)) {
560560
RegularText(text = stringResource(id = R.string.session_id))
561561
RegularText(text = sessionId)
562562
}
563563
}
564564

565565
@Composable
566566
fun ConnectionState(state: Boolean) {
567-
Column(verticalArrangement = Arrangement.spacedBy(Dimens.extraSmallSpacing)) {
567+
Column(verticalArrangement = Arrangement.spacedBy(Dimens.spacing4dp)) {
568568
RegularText(text = stringResource(id = R.string.socket))
569569
Row(
570570
horizontalArrangement = Arrangement.spacedBy(Dimens.extraSmallSpacing),
@@ -590,7 +590,7 @@ fun ConnectionState(state: Boolean) {
590590
@Composable
591591
fun CurrentCallState(state: CallState) {
592592
if (state == CallState.DONE) return
593-
Column(verticalArrangement = Arrangement.spacedBy(Dimens.extraSmallSpacing)) {
593+
Column(verticalArrangement = Arrangement.spacedBy(Dimens.spacing4dp)) {
594594
RegularText(text = stringResource(id = R.string.call_state))
595595
RegularText(text = state.name)
596596
}

compose_app/src/main/res/values/strings.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
<string name="off">Off</string>
3838
<string name="on">On</string>
3939
<string name="audio_settings">Audio Output Device</string>
40-
<string name="call_state">Call State:</string>
40+
<string name="call_state">Call State</string>
4141
<string name="show_wsmessages">WsMessages</string>
4242
<string name="clear_wsmessages">Clear WsMessages</string>
4343
<string name="share_wsmessages">Share WsMessages</string>

telnyx_common/src/main/AndroidManifest.xml

-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
33

44

5-
65
</manifest>

telnyx_common/src/main/java/com/telnyx/webrtc/common/TelnyxCommon.kt

+62-6
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ package com.telnyx.webrtc.common
33
import android.content.Context
44
import android.content.SharedPreferences
55
import androidx.lifecycle.Observer
6+
import com.telnyx.webrtc.common.service.CallForegroundService
67
import com.telnyx.webrtc.sdk.Call
78
import com.telnyx.webrtc.sdk.TelnyxClient
9+
import com.telnyx.webrtc.sdk.model.PushMetaData
10+
import com.telnyx.webrtc.sdk.verto.receive.InviteResponse
811
import kotlinx.coroutines.flow.MutableStateFlow
912
import kotlinx.coroutines.flow.StateFlow
13+
import timber.log.Timber
1014
import java.util.*
1115

1216
/**
@@ -19,7 +23,9 @@ class TelnyxCommon private constructor() {
1923
private var sharedPreferences: SharedPreferences? = null
2024
private val sharedPreferencesKey = "TelnyxCommonSharedPreferences"
2125

22-
private var telnyxClient: TelnyxClient? = null
26+
private var _telnyxClient: TelnyxClient? = null
27+
val telnyxClient
28+
get() = _telnyxClient
2329

2430
private var _currentCall: Call? = null
2531
val currentCall
@@ -31,6 +37,10 @@ class TelnyxCommon private constructor() {
3137

3238
private val holdStatusObservers: MutableMap<Call, Observer<Boolean>> = mutableMapOf()
3339

40+
private var _handlingPush = false
41+
val handlingPush
42+
get() = _handlingPush
43+
3444
companion object {
3545
@Volatile
3646
private var instance: TelnyxCommon? = null
@@ -47,18 +57,24 @@ class TelnyxCommon private constructor() {
4757
}
4858
}
4959

50-
internal fun setCurrentCall(call: Call?) {
60+
internal fun setCurrentCall(context: Context, call: Call?) {
5161
call?.let { newCall ->
5262
telnyxClient?.getActiveCalls()?.get(newCall.callId)?.let {
5363
_currentCall = it
64+
// Start the CallForegroundService - if one is not already running
65+
startCallService(context, it)
5466
}
5567
} ?: run {
5668
_currentCall = null
69+
// if we have no active call, stop the CallForegroundService
70+
stopCallService(context)
71+
// reset handling push flag, even if we were not previously handling push
72+
_handlingPush = false
5773
}
5874
}
5975

6076
internal fun registerCall(call: Call) {
61-
val holdStatusObserver = Observer<Boolean> { value ->
77+
val holdStatusObserver = Observer<Boolean> { _ ->
6278
updateHoldedCalls()
6379
}
6480
holdStatusObservers[call] = holdStatusObserver
@@ -71,16 +87,52 @@ class TelnyxCommon private constructor() {
7187
}
7288
}
7389

90+
private fun startCallService(viewContext: Context, call: Call?) {
91+
// Check if the service is already running
92+
if (CallForegroundService.isServiceRunning(viewContext)) {
93+
Timber.d("CallForegroundService is already running, not starting again")
94+
return
95+
}
96+
97+
val pushMetaData = PushMetaData(
98+
callerName = call?.inviteResponse?.callerIdName ?: "Active Call",
99+
callerNumber = call?.inviteResponse?.callerIdNumber ?: "",
100+
callId = call?.callId.toString(),
101+
)
102+
103+
try {
104+
// Start the foreground service
105+
CallForegroundService.startService(viewContext, pushMetaData)
106+
Timber.d("Started CallForegroundService for ongoing call")
107+
} catch (e: IllegalStateException) {
108+
Timber.e(e, "Failed to start CallForegroundService: ${e.message}")
109+
}
110+
}
111+
112+
internal fun stopCallService(context: Context) {
113+
try {
114+
context.let {
115+
if (CallForegroundService.isServiceRunning(it)) {
116+
CallForegroundService.stopService(it)
117+
Timber.d("Stopped CallForegroundService after call ended by remote party")
118+
}
119+
}
120+
} catch (e: IllegalStateException) {
121+
Timber.e(e, "Failed to stop CallForegroundService: ${e.message}")
122+
}
123+
}
124+
74125
internal fun getTelnyxClient(context: Context): TelnyxClient {
75-
return telnyxClient ?: synchronized(this) {
76-
telnyxClient ?: TelnyxClient(context.applicationContext).also { telnyxClient = it }
126+
return _telnyxClient ?: synchronized(this) {
127+
_telnyxClient ?: TelnyxClient(context.applicationContext).also { _telnyxClient = it }
77128
}
78129
}
79130

80131
internal fun resetTelnyxClient() {
81-
telnyxClient = null
132+
_telnyxClient = null
82133
}
83134

135+
84136
internal fun getSharedPreferences(context: Context): SharedPreferences {
85137
return sharedPreferences ?: synchronized(this) {
86138
sharedPreferences ?: context.getSharedPreferences(
@@ -90,6 +142,10 @@ class TelnyxCommon private constructor() {
90142
}
91143
}
92144

145+
internal fun setHandlingPush(value: Boolean) {
146+
_handlingPush = value
147+
}
148+
93149
private fun updateHoldedCalls() {
94150
_holdedCalls.value = telnyxClient?.getActiveCalls()?.entries?.filter { it.value.getIsOnHoldStatus().value == true }?.map { it.value } ?: emptyList()
95151
}

0 commit comments

Comments
 (0)