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.
This commit is contained in:
2026-08-21 03:15:22 +01:00
parent 899e778e49
commit a31d6a693c
7 changed files with 259 additions and 62 deletions

View File

@ -15,6 +15,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-feature android:name="android.hardware.type.watch" android:required="true" /> <uses-feature android:name="android.hardware.type.watch" android:required="true" />
@ -45,11 +46,20 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".InCallActivity"
android:exported="false"
android:taskAffinity=""
android:excludeFromRecents="true"
android:showWhenLocked="true"
android:turnScreenOn="true"
android:launchMode="singleTask" />
<service <service
android:name=".WearBaresipService" android:name=".WearBaresipService"
android:enabled="true" android:enabled="true"
android:exported="false" android:exported="false"
android:foregroundServiceType="microphone" android:foregroundServiceType="microphone|phoneCall"
android:stopWithTask="false" /> android:stopWithTask="false" />
<!-- Telecom ConnectionService (experiment): lets the stock Wear dialer <!-- Telecom ConnectionService (experiment): lets the stock Wear dialer
@ -64,4 +74,12 @@
</intent-filter> </intent-filter>
</service> </service>
</application> </application>
<!-- So RoleManager.isRoleAvailable(ROLE_DIALER) / requestRole resolve on
API 29+ even when the role-request activity isn't directly visible. -->
<queries>
<intent>
<action android:name="android.app.role.action.REQUEST_ROLE" />
</intent>
</queries>
</manifest> </manifest>

View File

@ -1,2 +1,2 @@
<sip:01273961147@mail.txt3.net>;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 <sip:01273961147@mail.txt3.net>;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

View File

@ -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}")
}
}
}

View File

@ -189,7 +189,11 @@ fun WearApp(activity: MainActivity) {
MaterialTheme { MaterialTheme {
Scaffold(timeText = { TimeText() }) { Scaffold(timeText = { TimeText() }) {
when { 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" }) activity.route.value == "accounts" -> AccountsScreen(onBack = { activity.route.value = "dialer" })
else -> DialerScreen(onAccounts = { activity.route.value = "accounts" }) else -> DialerScreen(onAccounts = { activity.route.value = "accounts" })
} }
@ -497,7 +501,11 @@ fun AccountsScreen(onBack: () -> Unit) {
} }
@Composable @Composable
fun InCallScreen(onEnd: () -> Unit) { fun InCallScreen(
onAnswer: (WearCall) -> Unit,
onDecline: (WearCall) -> Unit,
onEnd: (WearCall?) -> Unit
) {
val call = CallState.incoming() ?: CallState.active() val call = CallState.incoming() ?: CallState.active()
val status = call?.status ?: CallState.status.value val status = call?.status ?: CallState.status.value
val peer = call?.peerUri ?: "" val peer = call?.peerUri ?: ""
@ -533,11 +541,11 @@ fun InCallScreen(onEnd: () -> Unit) {
if (call != null && call.status == "incoming call") { if (call != null && call.status == "incoming call") {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = { answerCall(call) }) { Button(onClick = { onAnswer(call) }) {
Icon(Icons.Filled.Call, contentDescription = "Answer") Icon(Icons.Filled.Call, contentDescription = "Answer")
Text("Answer", modifier = Modifier.padding(start = 4.dp)) Text("Answer", modifier = Modifier.padding(start = 4.dp))
} }
Button(onClick = { declineCall(call) }) { Button(onClick = { onDecline(call) }) {
Icon(Icons.Filled.CallEnd, contentDescription = "Decline") Icon(Icons.Filled.CallEnd, contentDescription = "Decline")
Text("End", modifier = Modifier.padding(start = 4.dp)) Text("End", modifier = Modifier.padding(start = 4.dp))
} }
@ -551,7 +559,7 @@ fun InCallScreen(onEnd: () -> Unit) {
contentDescription = "Mute" contentDescription = "Mute"
) )
} }
Button(onClick = onEnd) { Button(onClick = { onEnd(call) }) {
Icon(Icons.Filled.CallEnd, contentDescription = "Hangup") Icon(Icons.Filled.CallEnd, contentDescription = "Hangup")
Text("End", modifier = Modifier.padding(start = 4.dp)) Text("End", modifier = Modifier.padding(start = 4.dp))
} }

View File

@ -4,6 +4,7 @@ import android.app.PendingIntent
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.telecom.PhoneAccount import android.telecom.PhoneAccount
import android.telecom.PhoneAccountHandle import android.telecom.PhoneAccountHandle
@ -11,17 +12,19 @@ import android.telecom.TelecomManager
import android.util.Log import android.util.Log
/** /**
* Registers our SIP identity as a telecom PhoneAccount and, on incoming calls, * Registers our SIP identity as a SELF-MANAGED telecom PhoneAccount and, on
* optionally hands the call to the system via TelecomManager.addNewIncomingCall * incoming calls, hands the call to the system via
* so the stock Wear incoming-call UI can be used. Whether the Wear dialer * TelecomManager.addNewIncomingCall. A self-managed account (CAPABILITY_SELF_MANAGED
* actually surfaces a third-party PhoneAccount is device/firmware dependent; * + MANAGE_OWN_CALLS, a normal permission) is the mechanism apps like WhatsApp
* this is wired as an experiment and the in-app InCallScreen remains the * use to show an incoming-call UI over the lock screen WITHOUT becoming the
* primary incoming-call UI. * 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 { object TelecomHelper {
private const val ACCOUNT_ID = "sip-wear" 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" private const val TAG = "Baresip Wear Telecom"
fun accountHandle(context: Context): PhoneAccountHandle { fun accountHandle(context: Context): PhoneAccountHandle {
@ -29,7 +32,7 @@ object TelecomHelper {
return PhoneAccountHandle(component, ACCOUNT_ID) 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) { fun registerAccount(context: Context) {
try { try {
val tm = context.getSystemService(TelecomManager::class.java) ?: return val tm = context.getSystemService(TelecomManager::class.java) ?: return
@ -42,8 +45,7 @@ object TelecomHelper {
) )
val account = PhoneAccount.builder(handle, "Baresip Wear (SIP)") val account = PhoneAccount.builder(handle, "Baresip Wear (SIP)")
.setCapabilities( .setCapabilities(
PhoneAccount.CAPABILITY_CALL_PROVIDER or PhoneAccount.CAPABILITY_SELF_MANAGED
PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING
) )
.setIcon( .setIcon(
android.graphics.drawable.Icon.createWithResource( android.graphics.drawable.Icon.createWithResource(
@ -52,17 +54,17 @@ object TelecomHelper {
) )
.build() .build()
tm.registerPhoneAccount(account) tm.registerPhoneAccount(account)
Log.d(TAG, "registered PhoneAccount $ACCOUNT_ID") Log.d(TAG, "registered self-managed PhoneAccount $ACCOUNT_ID")
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "registerAccount failed: ${e.message}") Log.w(TAG, "registerAccount failed: ${e.message}")
} }
} }
/** /**
* Hands an incoming native call to the system telecom stack. The framework * Hands an incoming native call to the system telecom stack as a
* will bind WearConnectionService and show the incoming-call UI if the Wear * self-managed call. The framework shows its incoming-call UI (over the
* dialer honors our account. Safe to call even if telecom ignores it -- the * lock screen) and forwards answer/decline to WearConnectionService. Our
* in-app InCallScreen is shown independently by the service. * in-app InCallActivity is the fallback if the system UI is suppressed.
*/ */
fun addIncomingCall(context: Context, callp: Long, uap: Long, peer: String) { fun addIncomingCall(context: Context, callp: Long, uap: Long, peer: String) {
try { try {

View File

@ -126,7 +126,17 @@ class WearBaresipService : Service() {
.setOngoing(true) .setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_SERVICE) .setCategory(NotificationCompat.CATEGORY_SERVICE)
.build() .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 // Hold the WiFi radio + CPU so inbound INVITEs can be answered even
// when the screen is off. // when the screen is off.
acquireWifiLock() acquireWifiLock()
@ -172,6 +182,16 @@ class WearBaresipService : Service() {
} }
nativeThread.start() 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
registerReceiver(networkReceiver, networkFilter(), Context.RECEIVER_NOT_EXPORTED) registerReceiver(networkReceiver, networkFilter(), Context.RECEIVER_NOT_EXPORTED)
} else { } else {
@ -309,9 +329,18 @@ class WearBaresipService : Service() {
// Track it so the Answer button can answer it. We do NOT answer // Track it so the Answer button can answer it. We do NOT answer
// here -- the user answers via the button (deferred to main loop). // here -- the user answers via the button (deferred to main loop).
ev == "call incoming" -> { ev == "call incoming" -> {
if (callp != 0L) { // The real, answerable struct call* is now available.
WearBaresipService.activeCallp = callp if (callp != 0L) WearBaresipService.activeCallp = callp
Log.d("Baresip Wear", "call incoming tracked callp=$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" ev == "call outgoing" -> CallState.status.value = "Calling"
@ -333,6 +362,12 @@ class WearBaresipService : Service() {
stopRinging() stopRinging()
dismissIncomingCallNotification() dismissIncomingCallNotification()
WearBaresipService.activeCallp = 0L 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. // Release any call wake lock now that the call is over.
try { wakeLock?.let { if (it.isHeld) it.release() } } catch (_: Exception) {} try { wakeLock?.let { if (it.isHeld) it.release() } } catch (_: Exception) {}
// Surface SIP failure reasons (e.g. "488 Not acceptable here") // 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 * 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, * from a background service does NOT reliably take over the watch face,
* so the robust mechanism is a high-priority notification with a * so the robust mechanism is to hand the call to the self-managed telecom
* full-screen intent that launches the InCallScreen over the clock. * stack (WearConnectionService), which launches InCallActivity over the
* We also wake the (usually dimmed) screen and buzz a ring pattern. * lock screen. We also wake the (usually dimmed) screen and buzz a ring
* pattern.
*/ */
private fun alertIncomingCall(peer: String) { private fun alertIncomingCall(peer: String) {
// Keep the CPU awake long enough to send the SIP 180/200 and present // The InCallActivity is launched from WearConnectionService (self-managed
// the heads-up. Released when the call ends. // 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) acquireWakeLock(60_000L)
// Wake the screen if it's asleep. // Wake the screen if it's asleep.
try { try {
@ -385,25 +423,32 @@ class WearBaresipService : Service() {
Log.w("Baresip Wear", "wake failed: ${e.message}") Log.w("Baresip Wear", "wake failed: ${e.message}")
} }
// Best-effort: also try to bring the activity forward (helps when the // NOTE: The incoming-call Activity is launched from
// app is already the foreground task and the full-screen intent is // WearConnectionService.onCreateIncomingConnection (the self-managed
// suppressed by the system). // 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 { try {
val intent = Intent(this, MainActivity::class.java).apply { @Suppress("DEPRECATION")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) val pm = getSystemService(android.os.PowerManager::class.java)
putExtra("incoming_call", true) val wl = pm?.newWakeLock(
} android.os.PowerManager.ACQUIRE_CAUSES_WAKEUP or
startActivity(intent) android.os.PowerManager.SCREEN_DIM_WAKE_LOCK,
Log.d("Baresip Wear", "alertIncomingCall: startActivity(incoming_call) dispatched") "BaresipWear:incomingWake"
)
wl?.acquire(5000)
} catch (e: Exception) { } 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 // High-priority heads-up with a full-screen intent -> the InCallScreen
// (Answer/Decline) is shown over the watch face. This is the reliable // (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 { 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) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
putExtra("incoming_call", true) putExtra("incoming_call", true)
} }

View File

@ -1,7 +1,11 @@
package com.tutpro.baresip.wear package com.tutpro.baresip.wear
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager
import android.telecom.Call.Details import android.telecom.Call.Details
import android.telecom.Connection import android.telecom.Connection
import android.telecom.ConnectionRequest import android.telecom.ConnectionRequest
@ -11,21 +15,20 @@ import android.telecom.PhoneAccountHandle
import android.util.Log 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 * When baresip reports "call incoming" we call TelecomManager.addNewIncomingCall
* account. The native baresip stack remains the source of truth for the call; * (see WearBaresipService / TelecomHelper). The framework binds this service and
* this service only mirrors telecom's incoming-call lifecycle so the system * calls onCreateIncomingConnection(). Because our PhoneAccount is SELF_MANAGED,
* incoming UI can be used if the Wear dialer chooses to honor our PhoneAccount. * 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: * The native baresip stack remains the source of truth for the call; this
* - Our service calls TelecomManager.addNewIncomingCall(handle, extras) when * service only mirrors the incoming-call lifecycle so the system can route
* baresip reports "call incoming"; the framework then binds this service and * answer / decline to the native stack via WearBaresipServiceHelper.
* 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.
*/ */
class WearConnectionService : ConnectionService() { class WearConnectionService : ConnectionService() {
@ -34,6 +37,54 @@ class WearConnectionService : ConnectionService() {
const val EXTRA_UAP = "com.tutpro.baresip.wear.extra.UAP" const val EXTRA_UAP = "com.tutpro.baresip.wear.extra.UAP"
const val EXTRA_PEER = "com.tutpro.baresip.wear.extra.PEER" const val EXTRA_PEER = "com.tutpro.baresip.wear.extra.PEER"
private const val TAG = "Baresip Wear Telecom" 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( override fun onCreateIncomingConnection(
@ -47,16 +98,18 @@ class WearConnectionService : ConnectionService() {
Log.d(TAG, "onCreateIncomingConnection callp=$callp uap=$uap peer=$peer") Log.d(TAG, "onCreateIncomingConnection callp=$callp uap=$uap peer=$peer")
val conn = WearConnection(callp, uap, peer) val conn = WearConnection(callp, uap, peer)
// Mirror the call for the system incoming UI. We deliberately avoid // Self-managed connection: the system expects OUR app to present the UI
// PROPERTY_SELF_MANAGED (requires MANAGE_OWN_CALLS permission and a // (it will not draw its own). CAPABILITY_MUTE lets the user mute.
// 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.
conn.connectionCapabilities = Connection.CAPABILITY_MUTE conn.connectionCapabilities = Connection.CAPABILITY_MUTE
conn.connectionProperties = Connection.PROPERTY_SELF_MANAGED
conn.setRinging() conn.setRinging()
// PRESENTATION_ALLOWED == 1 // PRESENTATION_ALLOWED == 1
conn.setAddress(Uri.parse(peer), 1) conn.setAddress(Uri.parse(peer), 1)
conn.setCallerDisplayName(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 return conn
} }
@ -73,7 +126,7 @@ class WearConnectionService : ConnectionService() {
} }
/** A telecom Connection mirroring one native baresip call. */ /** A telecom Connection mirroring one native baresip call. */
private class WearConnection( internal class WearConnection(
private val callp: Long, private val callp: Long,
private val uap: Long, private val uap: Long,
private val peer: String private val peer: String
@ -101,6 +154,7 @@ class WearConnectionService : ConnectionService() {
if (call != null) WearBaresipServiceHelper.hangup(call) if (call != null) WearBaresipServiceHelper.hangup(call)
setDisconnected(DisconnectCause(DisconnectCause.REJECTED)) setDisconnected(DisconnectCause(DisconnectCause.REJECTED))
destroy() destroy()
activeConnection = null
} }
override fun onDisconnect() { override fun onDisconnect() {
@ -110,6 +164,7 @@ class WearConnectionService : ConnectionService() {
if (call != null) WearBaresipServiceHelper.hangup(call) if (call != null) WearBaresipServiceHelper.hangup(call)
setDisconnected(DisconnectCause(DisconnectCause.LOCAL)) setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
destroy() destroy()
activeConnection = null
} }
override fun onPlayDtmfTone(c: Char) { override fun onPlayDtmfTone(c: Char) {