From 152c5c71b11c4cf0945a9c063ce029e59fab59b9 Mon Sep 17 00:00:00 2001 From: Muad'Dib <12030507+mwaddip@users.noreply.github.com> Date: Mon, 1 Jun 2026 04:24:53 +0200 Subject: [PATCH] fix(android): close session on dispose to prevent reconnect wedge (#15143) RemotePage.dispose() only reaches sessionClose at the tail of gFFI.close(), behind several awaits (canvas save, image update, the enable_soft_keyboard platform call). If the app is backgrounded while the page is disposing, dispose can be suspended before that runs, so the session is never torn down. The next reconnect re-attaches to the leaked session (mobile reuses a constant sessionId) and is stuck on "Connecting..." forever while the orphaned io_loop keeps streaming. Dispatch sessionClose at the start of dispose so teardown happens synchronously on route pop, before backgrounding can interrupt it. The sessionClose in gFFI.close() becomes a no-op once the session is already removed. Fixes #15060 Co-authored-by: Claude Opus 4.8 (1M context) --- flutter/lib/mobile/pages/remote_page.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 74a5af45c..fe664bf22 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -126,6 +126,14 @@ class _RemotePageState extends State with WidgetsBindingObserver { @override Future dispose() async { WidgetsBinding.instance.removeObserver(this); + // Close the session up-front. `gFFI.close()` below only calls `sessionClose` + // after several awaits (canvas save, image update, the `enable_soft_keyboard` + // platform call), so if the app is backgrounded while this page is disposing, + // dispose can be suspended before reaching it and the connection is never torn + // down. The reconnect then re-attaches to the leaked session and is stuck on + // "Connecting...". Dispatching it here makes teardown happen synchronously on + // pop; the `sessionClose` in `gFFI.close()` becomes a no-op once removed. + unawaited(bind.sessionClose(sessionId: sessionId)); // https://github.com/flutter/flutter/issues/64935 super.dispose(); gFFI.dialogManager.hideMobileActionsOverlay(store: false);