Improves stability and permission checks for voice calls when the app is in the background.

This commit is contained in:
Vitor Pamplona
2026-04-14 10:19:47 -04:00
parent 500bc8e4e7
commit 887e0bf302
3 changed files with 37 additions and 21 deletions
@@ -26,6 +26,7 @@ import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.call.AnswerRouteAction
import com.vitorpamplona.amethyst.commons.call.CallManager
import com.vitorpamplona.amethyst.commons.call.CallState
@@ -59,6 +60,7 @@ import java.util.concurrent.atomic.AtomicBoolean
private const val TAG = "CallController"
private const val VIDEO_MAX_BITRATE_BPS_DEFAULT = 1_500_000
@Stable
class CallController(
private val context: Context,
val callManager: CallManager,
@@ -656,11 +658,13 @@ class CallController(
try {
val peerName = callManager.currentPeerPubKey() ?: ""
val isVideo = mediaManager.isVideoEnabled.value
val isRinging = callManager.state.value is CallState.IncomingCall || callManager.state.value is CallState.Offering
val intent =
Intent(context, CallForegroundService::class.java).apply {
action = CallForegroundService.ACTION_START
putExtra(CallForegroundService.EXTRA_PEER_NAME, peerName)
putExtra(CallForegroundService.EXTRA_IS_VIDEO, isVideo)
putExtra(CallForegroundService.EXTRA_IS_RINGING, isRinging)
}
context.startForegroundService(intent)
} catch (e: Exception) {
@@ -672,10 +676,14 @@ class CallController(
if (!foregroundServiceStarted) return
try {
val peerName = callManager.currentPeerPubKey() ?: ""
val isVideo = mediaManager.isVideoEnabled.value
val isRinging = callManager.state.value is CallState.IncomingCall || callManager.state.value is CallState.Offering
val intent =
Intent(context, CallForegroundService::class.java).apply {
action = CallForegroundService.ACTION_UPDATE
putExtra(CallForegroundService.EXTRA_PEER_NAME, peerName)
putExtra(CallForegroundService.EXTRA_IS_VIDEO, isVideo)
putExtra(CallForegroundService.EXTRA_IS_RINGING, isRinging)
}
context.startService(intent)
} catch (e: Exception) {
@@ -50,6 +50,7 @@ class CallForegroundService : Service() {
const val EXTRA_PEER_NAME = "peer_name"
const val EXTRA_IS_VIDEO = "is_video"
const val EXTRA_STATUS_TEXT = "status_text"
const val EXTRA_IS_RINGING = "is_ringing"
private const val HANGUP_REQUEST_CODE = 0x70001
}
@@ -66,27 +67,32 @@ class CallForegroundService : Service() {
startId: Int,
): Int {
when (intent?.action) {
ACTION_START -> {
ACTION_START, ACTION_UPDATE -> {
val peerName = intent.getStringExtra(EXTRA_PEER_NAME) ?: "Unknown"
val isVideo = intent.getBooleanExtra(EXTRA_IS_VIDEO, false)
val isRinging = intent.getBooleanExtra(EXTRA_IS_RINGING, false)
val statusText = intent.getStringExtra(EXTRA_STATUS_TEXT)
val notification = buildNotification(peerName, statusText)
try {
val fgsType =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
var type = ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
if (!isRinging) {
val hasAudioPermission =
ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED
val hasCameraPermission =
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_GRANTED
try {
val fgsType =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
var type = ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
if (hasAudioPermission) {
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
}
if (isVideo && hasCameraPermission) {
type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
}
}
type
} else {
0
@@ -104,17 +110,11 @@ class CallForegroundService : Service() {
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, fallbackType)
} catch (e2: Exception) {
Log.e(TAG, "Foreground service start failed entirely", e2)
if (intent.action == ACTION_START) {
stopSelf()
}
}
}
ACTION_UPDATE -> {
val peerName = intent.getStringExtra(EXTRA_PEER_NAME) ?: "Unknown"
val statusText = intent.getStringExtra(EXTRA_STATUS_TEXT)
val notification = buildNotification(peerName, statusText)
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.notify(NOTIFICATION_ID, notification)
}
ACTION_STOP -> {
@@ -21,19 +21,24 @@
package com.vitorpamplona.amethyst.commons.call
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType
@Stable
@Immutable
sealed interface CallState {
@Stable
data object Idle : CallState
@Stable
data class Offering(
val callId: String,
val peerPubKeys: Set<HexKey>,
val callType: CallType,
) : CallState
@Stable
data class IncomingCall(
val callId: String,
val callerPubKey: HexKey,
@@ -42,6 +47,7 @@ sealed interface CallState {
val sdpOffer: String,
) : CallState
@Stable
data class Connecting(
val callId: String,
val peerPubKeys: Set<HexKey>,
@@ -49,6 +55,7 @@ sealed interface CallState {
val pendingPeerPubKeys: Set<HexKey> = emptySet(),
) : CallState
@Stable
data class Connected(
val callId: String,
val peerPubKeys: Set<HexKey>,
@@ -60,6 +67,7 @@ sealed interface CallState {
val allPeerPubKeys: Set<HexKey> get() = peerPubKeys + pendingPeerPubKeys
}
@Stable
data class Ended(
val callId: String,
val peerPubKeys: Set<HexKey>,