From a173b840be79d3b2cf729f817165223aac14c2ba Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:12:54 +0000 Subject: [PATCH 1/3] fix(client/android): ensure VpnTunnelService calls startForeground on all auto-start paths --- .../java/org/outline/vpn/VpnTunnelService.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java b/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java index 70f8c76b5b..3698b9bd34 100644 --- a/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java +++ b/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java @@ -475,10 +475,24 @@ private void broadcastVpnConnectivityChange(TunnelStatus status) { private void startLastSuccessfulTunnel() { LOG.info("Received an auto-connect request, loading last successful tunnel."); JSONObject tunnel = tunnelStore.load(); + + // Retrieve the server name early so we can pass it to startForegroundWithNotification. + // Fall back to an empty string when no tunnel is stored. + String serverName = ""; + if (tunnel != null) { + serverName = tunnel.optString(TUNNEL_SERVER_NAME, ""); + } + + // Always call startForeground before any early return. Android 8+ requires that a service + // started via startForegroundService() calls startForeground() within 5 seconds, regardless + // of the code path taken. Requires android.permission.FOREGROUND_SERVICE since Android P. + startForegroundWithNotification(serverName); + if (tunnel == null) { LOG.info("Last successful tunnel not found. User not connected at shutdown/install."); tunnelStore.setTunnelStatus(TunnelStatus.DISCONNECTED); QuickSettingsTileService.requestTileUpdate(this); + stopSelf(); return; } if (VpnTunnelService.prepare(VpnTunnelService.this) != null) { @@ -486,6 +500,7 @@ private void startLastSuccessfulTunnel() { LOG.warning("VPN not prepared, aborting auto-connect."); tunnelStore.setTunnelStatus(TunnelStatus.DISCONNECTED); QuickSettingsTileService.requestTileUpdate(this); + stopSelf(); return; } try { @@ -494,9 +509,6 @@ private void startLastSuccessfulTunnel() { tunnelConfig.name = tunnel.getString(TUNNEL_SERVER_NAME); tunnelConfig.transportConfig = tunnel.getString(TUNNEL_CONFIG_KEY); - // Start the service in the foreground as per Android 8+ background service execution limits. - // Requires android.permission.FOREGROUND_SERVICE since Android P. - startForegroundWithNotification(tunnelConfig.name); startTunnel(tunnelConfig, true); } catch (Exception e) { LOG.log(Level.SEVERE, "Failed to retrieve JSON tunnel data", e); From c81e09567d16dfb656918e0ee0138711c6a61c8b Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:18:16 +0000 Subject: [PATCH 2/3] fix(client/android): prevent ForegroundServiceDidNotStartInTimeException and stale notification on auto-connect failure --- .../src/main/java/org/outline/vpn/VpnTunnelService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java b/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java index 3698b9bd34..acd0e8bbd3 100644 --- a/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java +++ b/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java @@ -492,6 +492,9 @@ private void startLastSuccessfulTunnel() { LOG.info("Last successful tunnel not found. User not connected at shutdown/install."); tunnelStore.setTunnelStatus(TunnelStatus.DISCONNECTED); QuickSettingsTileService.requestTileUpdate(this); + // Remove the foreground notification explicitly: stopSelf() alone does not dismiss it while + // the service remains bound (e.g. via BIND_AUTO_CREATE from the app or crash recovery). + stopForeground(true); stopSelf(); return; } @@ -500,6 +503,7 @@ private void startLastSuccessfulTunnel() { LOG.warning("VPN not prepared, aborting auto-connect."); tunnelStore.setTunnelStatus(TunnelStatus.DISCONNECTED); QuickSettingsTileService.requestTileUpdate(this); + stopForeground(true); stopSelf(); return; } @@ -514,6 +518,8 @@ private void startLastSuccessfulTunnel() { LOG.log(Level.SEVERE, "Failed to retrieve JSON tunnel data", e); tunnelStore.setTunnelStatus(TunnelStatus.DISCONNECTED); QuickSettingsTileService.requestTileUpdate(this); + stopForeground(true); + stopSelf(); } } From 2275d959c390dd0a572d8889064b136f18c5792f Mon Sep 17 00:00:00 2001 From: fortuna <113565+fortuna@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:22:19 +0000 Subject: [PATCH 3/3] fix(client/android): ensure foreground service starts on auto-connect early exits Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com> --- .../org/outline/vpn/VpnTunnelService.java | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java b/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java index acd0e8bbd3..17bf547e36 100644 --- a/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java +++ b/client/src/cordova/android/OutlineAndroidLib/outline/src/main/java/org/outline/vpn/VpnTunnelService.java @@ -478,10 +478,7 @@ private void startLastSuccessfulTunnel() { // Retrieve the server name early so we can pass it to startForegroundWithNotification. // Fall back to an empty string when no tunnel is stored. - String serverName = ""; - if (tunnel != null) { - serverName = tunnel.optString(TUNNEL_SERVER_NAME, ""); - } + String serverName = tunnel != null ? tunnel.optString(TUNNEL_SERVER_NAME, "") : ""; // Always call startForeground before any early return. Android 8+ requires that a service // started via startForegroundService() calls startForeground() within 5 seconds, regardless @@ -490,21 +487,13 @@ private void startLastSuccessfulTunnel() { if (tunnel == null) { LOG.info("Last successful tunnel not found. User not connected at shutdown/install."); - tunnelStore.setTunnelStatus(TunnelStatus.DISCONNECTED); - QuickSettingsTileService.requestTileUpdate(this); - // Remove the foreground notification explicitly: stopSelf() alone does not dismiss it while - // the service remains bound (e.g. via BIND_AUTO_CREATE from the app or crash recovery). - stopForeground(true); - stopSelf(); + abortAutoConnect(); return; } if (VpnTunnelService.prepare(VpnTunnelService.this) != null) { // We cannot prepare the VPN when running as a background service, as it requires UI. LOG.warning("VPN not prepared, aborting auto-connect."); - tunnelStore.setTunnelStatus(TunnelStatus.DISCONNECTED); - QuickSettingsTileService.requestTileUpdate(this); - stopForeground(true); - stopSelf(); + abortAutoConnect(); return; } try { @@ -516,13 +505,28 @@ private void startLastSuccessfulTunnel() { startTunnel(tunnelConfig, true); } catch (Exception e) { LOG.log(Level.SEVERE, "Failed to retrieve JSON tunnel data", e); - tunnelStore.setTunnelStatus(TunnelStatus.DISCONNECTED); - QuickSettingsTileService.requestTileUpdate(this); - stopForeground(true); - stopSelf(); + abortAutoConnect(); } } + /** + * Cleans up after a failed auto-connect attempt: marks the tunnel as disconnected, refreshes the + * Quick Settings tile, removes the foreground notification, and stops the service. + * + *

Centralising these steps avoids repeating the same lifecycle transition in every early-exit + * path of {@link #startLastSuccessfulTunnel()}. + * + *

Note: {@link #stopForeground(boolean)} must be called explicitly because {@link #stopSelf()} + * alone does not dismiss the notification while the service remains bound (e.g. via + * BIND_AUTO_CREATE from the app or crash-recovery rebind). + */ + private void abortAutoConnect() { + tunnelStore.setTunnelStatus(TunnelStatus.DISCONNECTED); + QuickSettingsTileService.requestTileUpdate(this); + stopForeground(true); + stopSelf(); + } + private void storeActiveTunnel(@NonNull final TunnelConfig config) { LOG.info("Storing active tunnel."); JSONObject tunnel = new JSONObject();