From f86b8d1f94fba70dd5f07941e8460886f47fde50 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 19:23:34 +0000 Subject: [PATCH] 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 --- .../amethyst/service/call/CallController.kt | 29 ++++++++++++++++--- .../service/call/CallForegroundService.kt | 22 ++++++++++++-- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt index 359ba5331..355af1880 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallController.kt @@ -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 = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallForegroundService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallForegroundService.kt index d539cf7d2..b4a42cc2d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallForegroundService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/call/CallForegroundService.kt @@ -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)