Merge pull request #2206 from vitorpamplona/claude/review-webrtc-calls-0k3G4

fix: start foreground service earlier to prevent call death on Androi…
This commit is contained in:
Vitor Pamplona
2026-04-10 17:44:11 -04:00
committed by GitHub
2 changed files with 44 additions and 7 deletions
@@ -110,6 +110,7 @@ class CallController(
is CallState.Offering -> { is CallState.Offering -> {
audioManager.startRingbackTone() audioManager.startRingbackTone()
ensureForegroundService()
} }
is CallState.Connecting -> { is CallState.Connecting -> {
@@ -118,6 +119,7 @@ class CallController(
withContext(Dispatchers.IO) { audioManager.switchToCallAudioMode() } withContext(Dispatchers.IO) { audioManager.switchToCallAudioMode() }
audioManager.acquireProximityWakeLock() audioManager.acquireProximityWakeLock()
NotificationUtils.cancelCallNotification(context) NotificationUtils.cancelCallNotification(context)
updateForegroundServiceNotification()
} }
is CallState.Connected -> { is CallState.Connected -> {
@@ -126,6 +128,7 @@ class CallController(
withContext(Dispatchers.IO) { audioManager.switchToCallAudioMode() } withContext(Dispatchers.IO) { audioManager.switchToCallAudioMode() }
audioManager.acquireProximityWakeLock() audioManager.acquireProximityWakeLock()
NotificationUtils.cancelCallNotification(context) NotificationUtils.cancelCallNotification(context)
updateForegroundServiceNotification()
} }
is CallState.Ended -> { is CallState.Ended -> {
@@ -444,10 +447,7 @@ class CallController(
Log.d(TAG) { "Peer ${peerPubKey.take(8)} connected!" } Log.d(TAG) { "Peer ${peerPubKey.take(8)} connected!" }
scope.launch { scope.launch {
callManager.onPeerConnected() callManager.onPeerConnected()
if (!foregroundServiceStarted) { ensureForegroundService()
foregroundServiceStarted = true
startForegroundService()
}
} }
}, },
onRemoteVideoTrack = { track -> videoMonitor.onRemoteVideoTrack(peerPubKey, track) }, onRemoteVideoTrack = { track -> videoMonitor.onRemoteVideoTrack(peerPubKey, track) },
@@ -526,6 +526,12 @@ class CallController(
// ---- Foreground service ---- // ---- Foreground service ----
private fun ensureForegroundService() {
if (foregroundServiceStarted) return
foregroundServiceStarted = true
startForegroundService()
}
private fun startForegroundService() { private fun startForegroundService() {
try { try {
val peerName = callManager.currentPeerPubKey() ?: "" val peerName = callManager.currentPeerPubKey() ?: ""
@@ -542,6 +548,21 @@ class CallController(
} }
} }
private fun updateForegroundServiceNotification() {
if (!foregroundServiceStarted) return
try {
val peerName = callManager.currentPeerPubKey() ?: ""
val intent =
Intent(context, CallForegroundService::class.java).apply {
action = CallForegroundService.ACTION_UPDATE
putExtra(CallForegroundService.EXTRA_PEER_NAME, peerName)
}
context.startService(intent)
} catch (e: Exception) {
Log.e(TAG, "Failed to update foreground service notification", e)
}
}
private fun stopForegroundService() { private fun stopForegroundService() {
try { try {
val intent = val intent =
@@ -46,8 +46,10 @@ class CallForegroundService : Service() {
const val NOTIFICATION_ID = 9001 const val NOTIFICATION_ID = 9001
const val ACTION_START = "com.vitorpamplona.amethyst.CALL_START" const val ACTION_START = "com.vitorpamplona.amethyst.CALL_START"
const val ACTION_STOP = "com.vitorpamplona.amethyst.CALL_STOP" const val ACTION_STOP = "com.vitorpamplona.amethyst.CALL_STOP"
const val ACTION_UPDATE = "com.vitorpamplona.amethyst.CALL_UPDATE"
const val EXTRA_PEER_NAME = "peer_name" const val EXTRA_PEER_NAME = "peer_name"
const val EXTRA_IS_VIDEO = "is_video" const val EXTRA_IS_VIDEO = "is_video"
const val EXTRA_STATUS_TEXT = "status_text"
private const val HANGUP_REQUEST_CODE = 0x70001 private const val HANGUP_REQUEST_CODE = 0x70001
} }
@@ -67,7 +69,8 @@ class CallForegroundService : Service() {
ACTION_START -> { ACTION_START -> {
val peerName = intent.getStringExtra(EXTRA_PEER_NAME) ?: "Unknown" val peerName = intent.getStringExtra(EXTRA_PEER_NAME) ?: "Unknown"
val isVideo = intent.getBooleanExtra(EXTRA_IS_VIDEO, false) val isVideo = intent.getBooleanExtra(EXTRA_IS_VIDEO, false)
val notification = buildNotification(peerName) val statusText = intent.getStringExtra(EXTRA_STATUS_TEXT)
val notification = buildNotification(peerName, statusText)
val hasAudioPermission = val hasAudioPermission =
ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED PackageManager.PERMISSION_GRANTED
@@ -100,6 +103,14 @@ class CallForegroundService : Service() {
} }
} }
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 -> { ACTION_STOP -> {
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf() stopSelf()
@@ -121,7 +132,10 @@ class CallForegroundService : Service() {
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
} }
private fun buildNotification(peerName: String): Notification { private fun buildNotification(
peerName: String,
statusText: String? = null,
): Notification {
val openCallIntent = val openCallIntent =
PendingIntent.getActivity( PendingIntent.getActivity(
this, this,
@@ -142,10 +156,12 @@ class CallForegroundService : Service() {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
) )
val contentText = statusText ?: getString(R.string.call_with, peerName)
return NotificationCompat return NotificationCompat
.Builder(this, CHANNEL_ID) .Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.app_name)) .setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.call_with, peerName)) .setContentText(contentText)
.setSmallIcon(R.drawable.amethyst) .setSmallIcon(R.drawable.amethyst)
.setOngoing(true) .setOngoing(true)
.setContentIntent(openCallIntent) .setContentIntent(openCallIntent)