From a31d6a693ce63d3d8850f51d6a047bf553cf825b Mon Sep 17 00:00:00 2001 From: jamie prince Date: Fri, 21 Aug 2026 03:15:22 +0100 Subject: [PATCH] Wear SIP: incoming call UI over lock screen via self-managed telecom - Launch InCallActivity from WearConnectionService.onCreateIncomingConnection (self-managed telecom context) instead of a background FGS startActivity. This is the only path Android permits to show UI over a secure lock screen / from the background (BAL exemption); a plain FGS startActivity is blocked. - Register a SELF_MANAGED PhoneAccount (CAPABILITY_SELF_MANAGED only; a self-managed account must not also be CALL_PROVIDER) and hand inbound calls to TelecomManager.addNewIncomingCall. - Answer/Decline routed from the system self-managed Connection back to the native stack via WearBaresipServiceHelper (deferred ua_answer/ua_hangup). - Add dedicated InCallActivity (setShowWhenLocked/turnScreenOn) hosting the InCallScreen; MainActivity no longer owns the call UI. - Switch SIP transport to UDP:5761 (matches server); keep sipnat=outbound + natpinhole for NAT traversal. - Remove dead phoneCall FGS upgrade/downgrade (needed DIALER role; the self-managed launch path replaces it). Verified on Galaxy Watch (Android 16, SDK 36): inbound call rings + shows InCallActivity over the lock screen (pattern lock on) with working two-way audio; activity dismisses cleanly on hangup. --- wear/src/main/AndroidManifest.xml | 20 +++- wear/src/main/assets/accounts | 2 +- .../com/tutpro/baresip/wear/InCallActivity.kt | 69 ++++++++++++++ .../com/tutpro/baresip/wear/MainActivity.kt | 18 +++- .../com/tutpro/baresip/wear/TelecomHelper.kt | 32 ++++--- .../tutpro/baresip/wear/WearBaresipService.kt | 87 ++++++++++++----- .../baresip/wear/WearConnectionService.kt | 93 +++++++++++++++---- 7 files changed, 259 insertions(+), 62 deletions(-) create mode 100644 wear/src/main/java/com/tutpro/baresip/wear/InCallActivity.kt diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index ec419de6..f6245731 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ + @@ -45,11 +46,20 @@ + + + + + + + diff --git a/wear/src/main/assets/accounts b/wear/src/main/assets/accounts index e8df280b..ed927e3d 100644 --- a/wear/src/main/assets/accounts +++ b/wear/src/main/assets/accounts @@ -1,2 +1,2 @@ -;auth_user="01273961147";auth_pass="cisco55555";outbound="sip:mail.txt3.net:5060;transport=tcp";regint=60;regq=0.5;pubint=0;check_origin=no;mwi=no;sipnat=outbound;natpinhole=yes +;auth_user="01273961147";auth_pass="cisco55555";outbound="sip:mail.txt3.net:5761;transport=udp";regint=60;regq=0.5;pubint=0;check_origin=no;mwi=no;sipnat=outbound;natpinhole=yes diff --git a/wear/src/main/java/com/tutpro/baresip/wear/InCallActivity.kt b/wear/src/main/java/com/tutpro/baresip/wear/InCallActivity.kt new file mode 100644 index 00000000..74f39466 --- /dev/null +++ b/wear/src/main/java/com/tutpro/baresip/wear/InCallActivity.kt @@ -0,0 +1,69 @@ +package com.tutpro.baresip.wear + +import android.os.Build +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.* +import androidx.wear.compose.material.MaterialTheme +import kotlinx.coroutines.delay + +/** + * Dedicated incoming/active-call Activity, launched by the foreground service + * via a full-screen-intent notification (and a best-effort startActivity). + * + * Why a separate Activity from the LAUNCHER MainActivity: on modern Wear OS + * (Android 12+, this watch runs Android 16) a background service's + * startActivity / full-screen intent targeting the LAUNCHER activity is + * suppressed when that activity is not already the foreground task. A distinct, + * non-LAUNCHER Activity with showWhenLocked/turnScreenOn presents reliably over + * the lock screen / watch face for an inbound call. + */ +class InCallActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + applyCallLockScreenFlags() + setContent { + MaterialTheme { + // Auto-finish when there is no call left (answered+ended, or + // remote hangup cleared the state). Polls CallState so we don't + // need an explicit close from the service. + LaunchedEffect(Unit) { + while (CallState.incoming() != null || CallState.active() != null) { + delay(500) + } + finish() + } + InCallScreen( + onAnswer = { WearBaresipServiceHelper.answer(it) }, + onDecline = { WearBaresipServiceHelper.hangup(it) }, + onEnd = { call -> if (call != null) WearBaresipServiceHelper.hangup(call) } + ) + } + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + applyCallLockScreenFlags() + } + + @Suppress("DEPRECATION") + private fun applyCallLockScreenFlags() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(true) + setTurnScreenOn(true) + } + val params = window.attributes + params.flags = params.flags or + android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or + android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or + android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + window.attributes = params + } catch (e: Exception) { + android.util.Log.w("Baresip Wear", "InCallActivity flags failed: ${e.message}") + } + } +} diff --git a/wear/src/main/java/com/tutpro/baresip/wear/MainActivity.kt b/wear/src/main/java/com/tutpro/baresip/wear/MainActivity.kt index 0644aed0..e698f0bf 100644 --- a/wear/src/main/java/com/tutpro/baresip/wear/MainActivity.kt +++ b/wear/src/main/java/com/tutpro/baresip/wear/MainActivity.kt @@ -189,7 +189,11 @@ fun WearApp(activity: MainActivity) { MaterialTheme { Scaffold(timeText = { TimeText() }) { when { - hasCall -> InCallScreen(onEnd = { hangupCall(CallState.active() ?: CallState.incoming()) }) + hasCall -> InCallScreen( + onAnswer = { answerCall(it) }, + onDecline = { declineCall(it) }, + onEnd = { hangupCall(it) } + ) activity.route.value == "accounts" -> AccountsScreen(onBack = { activity.route.value = "dialer" }) else -> DialerScreen(onAccounts = { activity.route.value = "accounts" }) } @@ -497,7 +501,11 @@ fun AccountsScreen(onBack: () -> Unit) { } @Composable -fun InCallScreen(onEnd: () -> Unit) { +fun InCallScreen( + onAnswer: (WearCall) -> Unit, + onDecline: (WearCall) -> Unit, + onEnd: (WearCall?) -> Unit +) { val call = CallState.incoming() ?: CallState.active() val status = call?.status ?: CallState.status.value val peer = call?.peerUri ?: "" @@ -533,11 +541,11 @@ fun InCallScreen(onEnd: () -> Unit) { if (call != null && call.status == "incoming call") { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Button(onClick = { answerCall(call) }) { + Button(onClick = { onAnswer(call) }) { Icon(Icons.Filled.Call, contentDescription = "Answer") Text("Answer", modifier = Modifier.padding(start = 4.dp)) } - Button(onClick = { declineCall(call) }) { + Button(onClick = { onDecline(call) }) { Icon(Icons.Filled.CallEnd, contentDescription = "Decline") Text("End", modifier = Modifier.padding(start = 4.dp)) } @@ -551,7 +559,7 @@ fun InCallScreen(onEnd: () -> Unit) { contentDescription = "Mute" ) } - Button(onClick = onEnd) { + Button(onClick = { onEnd(call) }) { Icon(Icons.Filled.CallEnd, contentDescription = "Hangup") Text("End", modifier = Modifier.padding(start = 4.dp)) } diff --git a/wear/src/main/java/com/tutpro/baresip/wear/TelecomHelper.kt b/wear/src/main/java/com/tutpro/baresip/wear/TelecomHelper.kt index f504f887..170a645e 100644 --- a/wear/src/main/java/com/tutpro/baresip/wear/TelecomHelper.kt +++ b/wear/src/main/java/com/tutpro/baresip/wear/TelecomHelper.kt @@ -4,6 +4,7 @@ import android.app.PendingIntent import android.content.ComponentName import android.content.Context import android.content.Intent +import android.os.Build import android.os.Bundle import android.telecom.PhoneAccount import android.telecom.PhoneAccountHandle @@ -11,17 +12,19 @@ import android.telecom.TelecomManager import android.util.Log /** - * Registers our SIP identity as a telecom PhoneAccount and, on incoming calls, - * optionally hands the call to the system via TelecomManager.addNewIncomingCall - * so the stock Wear incoming-call UI can be used. Whether the Wear dialer - * actually surfaces a third-party PhoneAccount is device/firmware dependent; - * this is wired as an experiment and the in-app InCallScreen remains the - * primary incoming-call UI. + * Registers our SIP identity as a SELF-MANAGED telecom PhoneAccount and, on + * incoming calls, hands the call to the system via + * TelecomManager.addNewIncomingCall. A self-managed account (CAPABILITY_SELF_MANAGED + * + MANAGE_OWN_CALLS, a normal permission) is the mechanism apps like WhatsApp + * use to show an incoming-call UI over the lock screen WITHOUT becoming the + * device's default dialer. It is also exempt from the background-activity-launch + * (BAL) restriction, so the system call UI appears even when our app is in the + * background / the device is locked. */ object TelecomHelper { private const val ACCOUNT_ID = "sip-wear" - const val TELECOM_PERMISSION = android.Manifest.permission.READ_PHONE_STATE + const val TELECOM_PERMISSION = android.Manifest.permission.MANAGE_OWN_CALLS private const val TAG = "Baresip Wear Telecom" fun accountHandle(context: Context): PhoneAccountHandle { @@ -29,7 +32,7 @@ object TelecomHelper { return PhoneAccountHandle(component, ACCOUNT_ID) } - /** Register (or re-register) the PhoneAccount with the system telecom stack. */ + /** Register (or re-register) the self-managed PhoneAccount. */ fun registerAccount(context: Context) { try { val tm = context.getSystemService(TelecomManager::class.java) ?: return @@ -42,8 +45,7 @@ object TelecomHelper { ) val account = PhoneAccount.builder(handle, "Baresip Wear (SIP)") .setCapabilities( - PhoneAccount.CAPABILITY_CALL_PROVIDER or - PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING + PhoneAccount.CAPABILITY_SELF_MANAGED ) .setIcon( android.graphics.drawable.Icon.createWithResource( @@ -52,17 +54,17 @@ object TelecomHelper { ) .build() tm.registerPhoneAccount(account) - Log.d(TAG, "registered PhoneAccount $ACCOUNT_ID") + Log.d(TAG, "registered self-managed PhoneAccount $ACCOUNT_ID") } catch (e: Exception) { Log.w(TAG, "registerAccount failed: ${e.message}") } } /** - * Hands an incoming native call to the system telecom stack. The framework - * will bind WearConnectionService and show the incoming-call UI if the Wear - * dialer honors our account. Safe to call even if telecom ignores it -- the - * in-app InCallScreen is shown independently by the service. + * Hands an incoming native call to the system telecom stack as a + * self-managed call. The framework shows its incoming-call UI (over the + * lock screen) and forwards answer/decline to WearConnectionService. Our + * in-app InCallActivity is the fallback if the system UI is suppressed. */ fun addIncomingCall(context: Context, callp: Long, uap: Long, peer: String) { try { diff --git a/wear/src/main/java/com/tutpro/baresip/wear/WearBaresipService.kt b/wear/src/main/java/com/tutpro/baresip/wear/WearBaresipService.kt index 05202506..374b345f 100644 --- a/wear/src/main/java/com/tutpro/baresip/wear/WearBaresipService.kt +++ b/wear/src/main/java/com/tutpro/baresip/wear/WearBaresipService.kt @@ -126,7 +126,17 @@ class WearBaresipService : Service() { .setOngoing(true) .setCategory(NotificationCompat.CATEGORY_SERVICE) .build() - startForeground(NOTIFICATION_ID, notification) + // Start as microphone-only at boot: a phoneCall-type FGS is only valid + // once a call is actually active, so declaring phoneCall here (before + // any call) throws a SecurityException at startup. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + NOTIFICATION_ID, notification, + android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE + ) + } else { + startForeground(NOTIFICATION_ID, notification) + } // Hold the WiFi radio + CPU so inbound INVITEs can be answered even // when the screen is off. acquireWifiLock() @@ -172,6 +182,16 @@ class WearBaresipService : Service() { } nativeThread.start() + // Register our SIP identity as a telecom PhoneAccount so inbound calls + // can be surfaced via the system incoming-call UI (the only reliable + // way to show a call prompt over the lock screen from a background FGS + // on Android 12+, which blocks ordinary background Activity launches). + try { + TelecomHelper.registerAccount(this) + } catch (e: Exception) { + Log.w("Baresip Wear", "telecom register failed: ${e.message}") + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { registerReceiver(networkReceiver, networkFilter(), Context.RECEIVER_NOT_EXPORTED) } else { @@ -309,9 +329,18 @@ class WearBaresipService : Service() { // Track it so the Answer button can answer it. We do NOT answer // here -- the user answers via the button (deferred to main loop). ev == "call incoming" -> { - if (callp != 0L) { - WearBaresipService.activeCallp = callp - Log.d("Baresip Wear", "call incoming tracked callp=$callp") + // The real, answerable struct call* is now available. + if (callp != 0L) WearBaresipService.activeCallp = callp + Log.d("Baresip Wear", "call incoming tracked callp=$callp") + CallState.status.value = "Incoming call" + // Hand the call to the system telecom stack so the stock Wear + // incoming-call UI is shown over the lock screen (the only + // background-launch path Android allows). The in-app UI is a + // fallback that is normally blocked by the BAL restriction. + try { + TelecomHelper.addIncomingCall(this, callp, uap, arg.ifEmpty { "unknown" }) + } catch (e: Exception) { + Log.w("Baresip Wear", "telecom addIncomingCall failed: ${e.message}") } } ev == "call outgoing" -> CallState.status.value = "Calling" @@ -333,6 +362,12 @@ class WearBaresipService : Service() { stopRinging() dismissIncomingCallNotification() WearBaresipService.activeCallp = 0L + // Clear the system telecom UI if it was showing this call. + try { + WearConnectionService.disconnectActive(android.telecom.DisconnectCause.REMOTE) + } catch (e: Exception) { + Log.w("Baresip Wear", "telecom disconnect failed: ${e.message}") + } // Release any call wake lock now that the call is over. try { wakeLock?.let { if (it.isHeld) it.release() } } catch (_: Exception) {} // Surface SIP failure reasons (e.g. "488 Not acceptable here") @@ -362,13 +397,16 @@ class WearBaresipService : Service() { /** * Alert the wearer to an incoming call. On Wear OS a bare startActivity * from a background service does NOT reliably take over the watch face, - * so the robust mechanism is a high-priority notification with a - * full-screen intent that launches the InCallScreen over the clock. - * We also wake the (usually dimmed) screen and buzz a ring pattern. + * so the robust mechanism is to hand the call to the self-managed telecom + * stack (WearConnectionService), which launches InCallActivity over the + * lock screen. We also wake the (usually dimmed) screen and buzz a ring + * pattern. */ private fun alertIncomingCall(peer: String) { - // Keep the CPU awake long enough to send the SIP 180/200 and present - // the heads-up. Released when the call ends. + // The InCallActivity is launched from WearConnectionService (self-managed + // telecom context) when the system binds us for addNewIncomingCall — that + // path is permitted over the lock screen / from the background. + // Keep the CPU awake briefly so the SIP 180/200 and the heads-up render. acquireWakeLock(60_000L) // Wake the screen if it's asleep. try { @@ -385,25 +423,32 @@ class WearBaresipService : Service() { Log.w("Baresip Wear", "wake failed: ${e.message}") } - // Best-effort: also try to bring the activity forward (helps when the - // app is already the foreground task and the full-screen intent is - // suppressed by the system). + // NOTE: The incoming-call Activity is launched from + // WearConnectionService.onCreateIncomingConnection (the self-managed + // telecom context), which is the only path Android permits to show UI + // over the lock screen / from the background. A plain startActivity + // from this foreground service is BAL-blocked on a sleeping device and + // must NOT be used here. try { - val intent = Intent(this, MainActivity::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) - putExtra("incoming_call", true) - } - startActivity(intent) - Log.d("Baresip Wear", "alertIncomingCall: startActivity(incoming_call) dispatched") + @Suppress("DEPRECATION") + val pm = getSystemService(android.os.PowerManager::class.java) + val wl = pm?.newWakeLock( + android.os.PowerManager.ACQUIRE_CAUSES_WAKEUP or + android.os.PowerManager.SCREEN_DIM_WAKE_LOCK, + "BaresipWear:incomingWake" + ) + wl?.acquire(5000) } catch (e: Exception) { - Log.w("Baresip Wear", "bringToFront failed: ${e.message}") + Log.w("Baresip Wear", "wake failed: ${e.message}") } // High-priority heads-up with a full-screen intent -> the InCallScreen // (Answer/Decline) is shown over the watch face. This is the reliable - // alert path on Wear OS. + // alert path on Wear OS. A dedicated InCallActivity (not the LAUNCHER + // MainActivity) is used so the launch isn't suppressed when the app is + // backgrounded. try { - val fsIntent = Intent(this, MainActivity::class.java).apply { + val fsIntent = Intent(this, InCallActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) putExtra("incoming_call", true) } diff --git a/wear/src/main/java/com/tutpro/baresip/wear/WearConnectionService.kt b/wear/src/main/java/com/tutpro/baresip/wear/WearConnectionService.kt index 1d9ad926..09dbf284 100644 --- a/wear/src/main/java/com/tutpro/baresip/wear/WearConnectionService.kt +++ b/wear/src/main/java/com/tutpro/baresip/wear/WearConnectionService.kt @@ -1,7 +1,11 @@ package com.tutpro.baresip.wear +import android.app.KeyguardManager +import android.content.Context +import android.content.Intent import android.net.Uri import android.os.Bundle +import android.os.PowerManager import android.telecom.Call.Details import android.telecom.Connection import android.telecom.ConnectionRequest @@ -11,21 +15,20 @@ import android.telecom.PhoneAccountHandle import android.util.Log /** - * Telecom ConnectionService scaffold. + * Telecom ConnectionService — the self-managed call handler. * - * This is an EXPERIMENT to see whether the stock Wear dialer surfaces our SIP - * account. The native baresip stack remains the source of truth for the call; - * this service only mirrors telecom's incoming-call lifecycle so the system - * incoming UI can be used if the Wear dialer chooses to honor our PhoneAccount. + * When baresip reports "call incoming" we call TelecomManager.addNewIncomingCall + * (see WearBaresipService / TelecomHelper). The framework binds this service and + * calls onCreateIncomingConnection(). Because our PhoneAccount is SELF_MANAGED, + * the system does NOT draw its own incoming UI (letDialerHandleRinging=false); + * instead our app must present the call. The canonical self-managed pattern is + * to bring our own full-screen Activity forward from here — the active + * self-managed call grants the lock-screen / background-launch permission that a + * plain foreground-service startActivity lacks. * - * Lifecycle: - * - Our service calls TelecomManager.addNewIncomingCall(handle, extras) when - * baresip reports "call incoming"; the framework then binds this service and - * calls onCreateIncomingConnection(). - * - onCreateIncomingConnection() builds a WearConnection bound to the native - * callp (passed via extras) and reports the remote address. - * - User actions (answer / disconnect / DTMF) are forwarded to the native - * stack through WearBaresipServiceHelper. + * The native baresip stack remains the source of truth for the call; this + * service only mirrors the incoming-call lifecycle so the system can route + * answer / decline to the native stack via WearBaresipServiceHelper. */ class WearConnectionService : ConnectionService() { @@ -34,6 +37,54 @@ class WearConnectionService : ConnectionService() { const val EXTRA_UAP = "com.tutpro.baresip.wear.extra.UAP" const val EXTRA_PEER = "com.tutpro.baresip.wear.extra.PEER" private const val TAG = "Baresip Wear Telecom" + + // The connection currently shown by the system incoming/active UI. + // Cleared when the call ends so a later call can replace it. + @Volatile + var activeConnection: android.telecom.Connection? = null + private set + + /** Tell the system UI the call ended (remote hangup / local hangup). */ + fun disconnectActive(cause: Int = DisconnectCause.REMOTE) { + val conn = activeConnection ?: return + try { + conn.setDisconnected(DisconnectCause(cause)) + conn.destroy() + } catch (e: Exception) { + Log.w(TAG, "disconnectActive failed: ${e.message}") + } + activeConnection = null + } + + /** + * Wake the screen and bring our InCallActivity to the front. Called from + * the self-managed connection context so the active incoming call + * grants the lock-screen / background launch permission. + */ + private fun showInCallUi(context: Context, callp: Long, uap: Long, peer: String) { + try { + val pm = context.getSystemService(PowerManager::class.java) + val wakeLock = pm?.newWakeLock( + PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.FULL_WAKE_LOCK, + "BaresipWear:incomingCall" + ) + wakeLock?.acquire(60_000L) + val intent = Intent(context, InCallActivity::class.java).apply { + addFlags( + Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_CLEAR_TOP or + Intent.FLAG_ACTIVITY_SINGLE_TOP + ) + putExtra(EXTRA_CALLP, callp) + putExtra(EXTRA_UAP, uap) + putExtra(EXTRA_PEER, peer) + } + context.startActivity(intent) + Log.d(TAG, "showInCallUi: InCallActivity started (callp=$callp)") + } catch (e: Exception) { + Log.w(TAG, "showInCallUi failed: ${e.message}") + } + } } override fun onCreateIncomingConnection( @@ -47,16 +98,18 @@ class WearConnectionService : ConnectionService() { Log.d(TAG, "onCreateIncomingConnection callp=$callp uap=$uap peer=$peer") val conn = WearConnection(callp, uap, peer) - // Mirror the call for the system incoming UI. We deliberately avoid - // PROPERTY_SELF_MANAGED (requires MANAGE_OWN_CALLS permission and a - // more involved self-managed lifecycle) and video capabilities that - // are absent on this Wear SDK. Plain CAPABILITY_MUTE is enough for the - // call to be presented. + // Self-managed connection: the system expects OUR app to present the UI + // (it will not draw its own). CAPABILITY_MUTE lets the user mute. conn.connectionCapabilities = Connection.CAPABILITY_MUTE + conn.connectionProperties = Connection.PROPERTY_SELF_MANAGED conn.setRinging() // PRESENTATION_ALLOWED == 1 conn.setAddress(Uri.parse(peer), 1) conn.setCallerDisplayName(peer, 1) + activeConnection = conn + // Bring our incoming-call UI forward (over lock screen / from the + // background) while the self-managed call is active. + showInCallUi(this, callp, uap, peer) return conn } @@ -73,7 +126,7 @@ class WearConnectionService : ConnectionService() { } /** A telecom Connection mirroring one native baresip call. */ - private class WearConnection( + internal class WearConnection( private val callp: Long, private val uap: Long, private val peer: String @@ -101,6 +154,7 @@ class WearConnectionService : ConnectionService() { if (call != null) WearBaresipServiceHelper.hangup(call) setDisconnected(DisconnectCause(DisconnectCause.REJECTED)) destroy() + activeConnection = null } override fun onDisconnect() { @@ -110,6 +164,7 @@ class WearConnectionService : ConnectionService() { if (call != null) WearBaresipServiceHelper.hangup(call) setDisconnected(DisconnectCause(DisconnectCause.LOCAL)) destroy() + activeConnection = null } override fun onPlayDtmfTone(c: Char) {