fix: start foreground service earlier to prevent call death on Android 14+

The foreground service was only started after onPeerConnected, leaving
the Offering/Connecting phases unprotected. If the user backgrounded
the app during connecting, Android 14+ could block the later
startForegroundService() call, killing the call.

Changes:
- Start foreground service on Offering state (user just tapped call
  button, so app is guaranteed to be in foreground)
- Update notification text on Connecting/Connected transitions
- Add ACTION_UPDATE to CallForegroundService to change notification
  without restarting the service
- onPeerConnected now uses ensureForegroundService() as a safety net

https://claude.ai/code/session_01F5RF2yzngiMr1v2gr7f1GP
This commit is contained in:
Claude
2026-04-10 19:23:34 +00:00
parent cd573a583c
commit f86b8d1f94
2 changed files with 44 additions and 7 deletions
@@ -110,6 +110,7 @@ class CallController(
is CallState.Offering -> {
audioManager.startRingbackTone()
ensureForegroundService()
}
is CallState.Connecting -> {
@@ -118,6 +119,7 @@ class CallController(
withContext(Dispatchers.IO) { audioManager.switchToCallAudioMode() }
audioManager.acquireProximityWakeLock()
NotificationUtils.cancelCallNotification(context)
updateForegroundServiceNotification()
}
is CallState.Connected -> {
@@ -126,6 +128,7 @@ class CallController(
withContext(Dispatchers.IO) { audioManager.switchToCallAudioMode() }
audioManager.acquireProximityWakeLock()
NotificationUtils.cancelCallNotification(context)
updateForegroundServiceNotification()
}
is CallState.Ended -> {
@@ -444,10 +447,7 @@ class CallController(
Log.d(TAG) { "Peer ${peerPubKey.take(8)} connected!" }
scope.launch {
callManager.onPeerConnected()
if (!foregroundServiceStarted) {
foregroundServiceStarted = true
startForegroundService()
}
ensureForegroundService()
}
},
onRemoteVideoTrack = { track -> videoMonitor.onRemoteVideoTrack(peerPubKey, track) },
@@ -526,6 +526,12 @@ class CallController(
// ---- Foreground service ----
private fun ensureForegroundService() {
if (foregroundServiceStarted) return
foregroundServiceStarted = true
startForegroundService()
}
private fun startForegroundService() {
try {
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() {
try {
val intent =
@@ -46,8 +46,10 @@ class CallForegroundService : Service() {
const val NOTIFICATION_ID = 9001
const val ACTION_START = "com.vitorpamplona.amethyst.CALL_START"
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_IS_VIDEO = "is_video"
const val EXTRA_STATUS_TEXT = "status_text"
private const val HANGUP_REQUEST_CODE = 0x70001
}
@@ -67,7 +69,8 @@ class CallForegroundService : Service() {
ACTION_START -> {
val peerName = intent.getStringExtra(EXTRA_PEER_NAME) ?: "Unknown"
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 =
ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) ==
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 -> {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
@@ -121,7 +132,10 @@ class CallForegroundService : Service() {
notificationManager.createNotificationChannel(channel)
}
private fun buildNotification(peerName: String): Notification {
private fun buildNotification(
peerName: String,
statusText: String? = null,
): Notification {
val openCallIntent =
PendingIntent.getActivity(
this,
@@ -142,10 +156,12 @@ class CallForegroundService : Service() {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
val contentText = statusText ?: getString(R.string.call_with, peerName)
return NotificationCompat
.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.call_with, peerName))
.setContentText(contentText)
.setSmallIcon(R.drawable.amethyst)
.setOngoing(true)
.setContentIntent(openCallIntent)