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:
@ -15,6 +15,7 @@
|
||||
<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_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
||||
|
||||
<uses-feature android:name="android.hardware.type.watch" android:required="true" />
|
||||
|
||||
@ -45,11 +46,20 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".InCallActivity"
|
||||
android:exported="false"
|
||||
android:taskAffinity=""
|
||||
android:excludeFromRecents="true"
|
||||
android:showWhenLocked="true"
|
||||
android:turnScreenOn="true"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<service
|
||||
android:name=".WearBaresipService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="microphone"
|
||||
android:foregroundServiceType="microphone|phoneCall"
|
||||
android:stopWithTask="false" />
|
||||
|
||||
<!-- Telecom ConnectionService (experiment): lets the stock Wear dialer
|
||||
@ -64,4 +74,12 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
</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>
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
69
wear/src/main/java/com/tutpro/baresip/wear/InCallActivity.kt
Normal file
69
wear/src/main/java/com/tutpro/baresip/wear/InCallActivity.kt
Normal 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user