Merge pull request #2107 from vitorpamplona/claude/fix-call-ringing-issue-OGVEG

Fix call audio state transitions and notification handling
This commit is contained in:
Vitor Pamplona
2026-04-03 14:19:23 -04:00
committed by GitHub
4 changed files with 75 additions and 17 deletions
@@ -107,6 +107,7 @@ class CallAudioManager(
}
fun startRingbackTone() {
stopRingbackTone()
try {
ringbackTone =
ToneGenerator(AudioManager.STREAM_VOICE_CALL, 80).also {
@@ -123,6 +124,7 @@ class CallAudioManager(
}
fun switchToCallAudioMode() {
if (audioManager.mode == AudioManager.MODE_IN_COMMUNICATION) return
previousAudioMode = audioManager.mode
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
@@ -177,7 +177,15 @@ class CallController(
}
is CallState.Connected -> {
// Stop ringing/ringback in case the Connecting state
// was skipped due to StateFlow conflation (the value
// can change Offering → Connecting → Connected before
// the collector processes Connecting).
audioManager.stopRinging()
audioManager.stopRingbackTone()
withContext(Dispatchers.IO) { audioManager.switchToCallAudioMode() }
audioManager.acquireProximityWakeLock()
NotificationUtils.cancelCallNotification(context)
}
is CallState.Ended -> {
@@ -185,12 +193,11 @@ class CallController(
}
is CallState.Idle -> {
// Safety net: ensure ringing and notifications are
// stopped even if the Ended state was missed due to
// StateFlow conflation.
audioManager.stopRinging()
audioManager.stopRingbackTone()
NotificationUtils.cancelCallNotification(context)
// Safety net: full cleanup in case the Ended state
// was missed due to StateFlow conflation. cleanup()
// is idempotent — calling it twice is harmless because
// each resource is null-checked and nulled out.
cleanup()
}
}
}
@@ -729,24 +736,71 @@ class CallController(
// ---- Cleanup ----
fun cleanup() {
audioManager.release()
stopForegroundService()
// Each block is wrapped individually so that a failure in one
// (e.g. a WebRTC native crash) does not prevent the rest from
// running. Without this, a single exception could leave the
// camera open, audio mode stuck, or the foreground service alive.
try {
audioManager.release()
} catch (e: Exception) {
Log.e(TAG, "cleanup: audioManager.release() failed", e)
}
try {
stopForegroundService()
} catch (e: Exception) {
Log.e(TAG, "cleanup: stopForegroundService() failed", e)
}
foregroundServiceStarted = false
NotificationUtils.cancelCallNotification(context)
stopRemoteVideoMonitor()
// Dispose all peer sessions
peerSessions.values.forEach { it.session.dispose() }
for (ps in peerSessions.values) {
try {
ps.session.dispose()
} catch (e: Exception) {
Log.e(TAG, "cleanup: PeerSession.dispose() failed", e)
}
}
peerSessions.clear()
// Dispose shared resources
stopCamera()
localAudioTrackInternal?.dispose()
localVideoTrackInternal?.dispose()
localAudioSource?.dispose()
localVideoSource?.dispose()
peerConnectionFactory?.dispose()
sharedEglBase?.release()
// Dispose shared resources — each in its own try-catch so one
// failure does not prevent the others from being released.
try {
stopCamera()
} catch (e: Exception) {
Log.e(TAG, "cleanup: stopCamera() failed", e)
}
try {
localAudioTrackInternal?.dispose()
} catch (e: Exception) {
Log.e(TAG, "cleanup: localAudioTrack.dispose() failed", e)
}
try {
localVideoTrackInternal?.dispose()
} catch (e: Exception) {
Log.e(TAG, "cleanup: localVideoTrack.dispose() failed", e)
}
try {
localAudioSource?.dispose()
} catch (e: Exception) {
Log.e(TAG, "cleanup: localAudioSource.dispose() failed", e)
}
try {
localVideoSource?.dispose()
} catch (e: Exception) {
Log.e(TAG, "cleanup: localVideoSource.dispose() failed", e)
}
try {
peerConnectionFactory?.dispose()
} catch (e: Exception) {
Log.e(TAG, "cleanup: peerConnectionFactory.dispose() failed", e)
}
try {
sharedEglBase?.release()
} catch (e: Exception) {
Log.e(TAG, "cleanup: sharedEglBase.release() failed", e)
}
localAudioTrackInternal = null
localVideoTrackInternal = null
@@ -1429,6 +1429,7 @@ class AccountViewModel(
override fun onCleared() {
Log.d("AccountViewModel", "onCleared")
callController?.cleanup()
feedStates.destroy()
super.onCleared()
}
@@ -606,6 +606,7 @@ class CallManager(
delay(ENDED_DISPLAY_MS)
if (_state.value is CallState.Ended) {
_state.value = CallState.Idle
processedEventIds.clear()
}
}
}