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..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 @@ -475,17 +475,25 @@ 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 = 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 + // 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); + 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); + abortAutoConnect(); return; } try { @@ -494,17 +502,31 @@ 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); - tunnelStore.setTunnelStatus(TunnelStatus.DISCONNECTED); - QuickSettingsTileService.requestTileUpdate(this); + 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();