@@ -81,10 +81,8 @@ class ServiceClient(private val settings: SettingsRepository) : CoroutineScope,
8181 private var lastWebRTCConnection: WebRTCConnectionCache ? = null
8282
8383 // --- Lifecycle / background state ---
84- var isAnythingPlayingFlow: StateFlow <Boolean >? = null
8584 private var isInBackground = false
8685 private var hasActiveExternalConsumer = false
87- private var backgroundPlaybackMonitorJob: Job ? = null
8886
8987 private sealed class BackgroundedConnectionInfo {
9088 data class Direct (val connectionInfo : ConnectionInfo ) : BackgroundedConnectionInfo()
@@ -163,14 +161,11 @@ class ServiceClient(private val settings: SettingsRepository) : CoroutineScope,
163161
164162 /* *
165163 * Called when the app moves to the background.
166- * If nothing is playing, disconnect to save resources. If something is playing,
167- * keep the connection alive but monitor — if playback stops while still backgrounded,
168- * disconnect at that point.
164+ * Connection stays alive — if the system kills it, we save info and reconnect on foreground.
169165 */
170166 fun onAppBackground () {
171167 isInBackground = true
172168 logger.i { " App backgrounded" }
173- evaluateBackgroundDisconnect()
174169 }
175170
176171 /* *
@@ -201,73 +196,10 @@ class ServiceClient(private val settings: SettingsRepository) : CoroutineScope,
201196
202197 /* *
203198 * Called when an external consumer (Android Auto / CarPlay) becomes inactive.
204- * Re-evaluates whether a background disconnect is needed.
205199 */
206200 fun onExternalConsumerInactive () {
207201 hasActiveExternalConsumer = false
208202 logger.i { " External consumer inactive" }
209-
210- if (isInBackground) {
211- evaluateBackgroundDisconnect()
212- }
213- }
214-
215- /* *
216- * Evaluates whether to disconnect due to backgrounding.
217- * Skipped when an external consumer (Android Auto / CarPlay) is active.
218- */
219- private fun evaluateBackgroundDisconnect () {
220-
221- if (hasActiveExternalConsumer) {
222- logger.i { " External consumer active — skipping background disconnect" }
223- return
224- }
225-
226- val currentState = _sessionState .value
227-
228- // Only act if connected or reconnecting
229- if (currentState !is SessionState .Connected && currentState !is SessionState .Reconnecting ) {
230- logger.d { " Not connected, nothing to do on background" }
231- return
232- }
233-
234- if (isAnythingPlayingFlow?.value == true ) {
235- logger.i { " Audio is playing — keeping connection alive, monitoring playback" }
236- backgroundPlaybackMonitorJob?.cancel()
237- backgroundPlaybackMonitorJob = launch {
238- isAnythingPlayingFlow?.collect { isPlaying ->
239- if (! isPlaying && isInBackground && ! hasActiveExternalConsumer) {
240- logger.i { " Playback stopped while backgrounded — disconnecting now" }
241- performBackgroundDisconnect()
242- // Stop monitoring after disconnect (coroutine is still alive until next suspension)
243- return @collect
244- }
245- }
246- }
247- return
248- }
249-
250- performBackgroundDisconnect()
251- }
252-
253- private fun performBackgroundDisconnect () {
254- val currentState = _sessionState .value
255-
256- // Save connection info for foreground reconnect
257- backgroundedConnectionInfo = when (currentState) {
258- is SessionState .Connected .Direct ->
259- BackgroundedConnectionInfo .Direct (currentState.connectionInfo)
260- is SessionState .Reconnecting .Direct ->
261- BackgroundedConnectionInfo .Direct (currentState.connectionInfo)
262- is SessionState .Connected .WebRTC ->
263- BackgroundedConnectionInfo .WebRTC (currentState.remoteId)
264- is SessionState .Reconnecting .WebRTC ->
265- BackgroundedConnectionInfo .WebRTC (currentState.remoteId)
266- else -> null
267- }
268-
269- logger.i { " Disconnecting (Backgrounded), saved=${backgroundedConnectionInfo != null } " }
270- disconnect(SessionState .Disconnected .Backgrounded )
271203 }
272204
273205 /* *
@@ -276,8 +208,6 @@ class ServiceClient(private val settings: SettingsRepository) : CoroutineScope,
276208 */
277209 fun onAppForeground () {
278210 isInBackground = false
279- backgroundPlaybackMonitorJob?.cancel()
280- backgroundPlaybackMonitorJob = null
281211 logger.i { " App foregrounded" }
282212
283213 val savedInfo = backgroundedConnectionInfo ? : return
@@ -703,6 +633,14 @@ class ServiceClient(private val settings: SettingsRepository) : CoroutineScope,
703633 return @collect
704634 }
705635
636+ // If backgrounded without external consumer, save info and stop
637+ if (isInBackground && ! hasActiveExternalConsumer) {
638+ logger.i { " App is backgrounded — saving WebRTC connection info instead of reconnecting" }
639+ backgroundedConnectionInfo = BackgroundedConnectionInfo .WebRTC (info.remoteId)
640+ disconnect(SessionState .Disconnected .Backgrounded )
641+ return @collect
642+ }
643+
706644 val source =
707645 if (currentState is SessionState .Connected .WebRTC ) " current state" else " cache"
708646 logger
@@ -1016,6 +954,14 @@ class ServiceClient(private val settings: SettingsRepository) : CoroutineScope,
1016954 }
1017955 )
1018956
957+ // If backgrounded during reconnection, save info and disconnect instead of re-auth
958+ if (isInBackground && ! hasActiveExternalConsumer && _sessionState .value is SessionState .Reconnecting .WebRTC ) {
959+ logger.i { " App backgrounded during WebRTC reconnection — saving info and disconnecting" }
960+ backgroundedConnectionInfo = BackgroundedConnectionInfo .WebRTC (remoteId)
961+ disconnect(SessionState .Disconnected .Backgrounded )
962+ return
963+ }
964+
1019965 // WebRTC-specific: re-authenticate with saved token after successful reconnection
1020966 if (_sessionState .value is SessionState .Connected .WebRTC ) {
1021967 logger
@@ -1111,6 +1057,14 @@ class ServiceClient(private val settings: SettingsRepository) : CoroutineScope,
11111057 val authProcessState = state.authProcessState
11121058 val wasAutoLogin = state.wasAutoLogin
11131059
1060+ // If backgrounded without external consumer, save info and disconnect
1061+ if (isInBackground && ! hasActiveExternalConsumer) {
1062+ logger.i { " App is backgrounded — saving Direct connection info instead of reconnecting" }
1063+ backgroundedConnectionInfo = BackgroundedConnectionInfo .Direct (connectionInfo)
1064+ disconnect(SessionState .Disconnected .Backgrounded )
1065+ return
1066+ }
1067+
11141068 // Enter Reconnecting state (preserves server/user/auth state - no UI reload!)
11151069 _sessionState .update {
11161070 SessionState .Reconnecting .Direct (
@@ -1152,6 +1106,13 @@ class ServiceClient(private val settings: SettingsRepository) : CoroutineScope,
11521106 disconnect(SessionState .Disconnected .Error (Exception (" Failed to reconnect after max attempts" )))
11531107 }
11541108 )
1109+
1110+ // If backgrounded during reconnection, save info and disconnect
1111+ if (isInBackground && ! hasActiveExternalConsumer && _sessionState .value is SessionState .Reconnecting .Direct ) {
1112+ logger.i { " App backgrounded during Direct reconnection — saving info and disconnecting" }
1113+ backgroundedConnectionInfo = BackgroundedConnectionInfo .Direct (connectionInfo)
1114+ disconnect(SessionState .Disconnected .Backgrounded )
1115+ }
11551116 }
11561117
11571118 suspend fun sendRequest (request : Request ): Result <Answer > = suspendCoroutine { continuation ->
0 commit comments