Skip to content

Commit 45f312c

Browse files
committed
[bugfix] connection
1 parent 8e05352 commit 45f312c

11 files changed

Lines changed: 122 additions & 37 deletions

File tree

app/src/main/cpp/src/wrappers/wrapper_websocket_client/wrapper_websocket_client.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,13 @@ void WrapperWebsocketClient::onConnectedCallback() {
309309
reconnection_attempts_ = kMaxReconnectionAttempts_;
310310
window_start_time_ = std::chrono::steady_clock::now();
311311

312+
// onOpenImpl fires only once per object lifetime
313+
bool expected = false;
314+
if (!has_opened_.compare_exchange_strong(expected, true)) {
315+
SPDLOG_INFO("onConnectedCallback: already notified Java once — skipping onOpenImpl");
316+
return;
317+
}
318+
312319
JNIEnv* env = getJniEnv();
313320
if (!env) {
314321
SPDLOG_ERROR("Failed to get JNI environment in onConnectedCallback");

app/src/main/cpp/src/wrappers/wrapper_websocket_client/wrapper_websocket_client.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,16 @@ class WrapperWebsocketClient final {
6565
const fptn::common::network::IPv6Address& ipv6);
6666

6767
private:
68-
const int kMaxReconnectionAttempts_ = 2;
68+
const int kMaxReconnectionAttempts_ = 3;
6969

7070
std::thread th_;
7171
mutable std::mutex mutex_;
7272
mutable std::atomic<bool> running_;
7373
mutable std::atomic<int> reconnection_attempts_;
7474
std::chrono::steady_clock::time_point window_start_time_;
75+
// onOpenImpl must fire only once per object lifetime — set on first successful
76+
// connect, never cleared until this object is destroyed and a new one is created.
77+
std::atomic<bool> has_opened_{false};
7578

7679
const jobject wrapper_;
7780

app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ public class FptnConnection extends Thread {
115115
private FileOutputStream outputStream;
116116

117117
private volatile boolean tunNeedsRecreate = false;
118+
private volatile String pendingShutdownReason = null;
118119

119120
@Getter
120121
private Instant connectionTime;
@@ -136,8 +137,6 @@ public class FptnConnection extends Thread {
136137
private final AdBlocker adBlocker; // null when ad blocking is disabled
137138
private final String customDnsIpv4; // null when custom DNS is disabled
138139

139-
private final Object webSocketLock = new Object();
140-
141140
public FptnConnection(final FptnService service,
142141
final int connectionId,
143142
final ServerEntity serverEntity,
@@ -198,15 +197,22 @@ public void run() {
198197
byte[] byteBuffer = new byte[MAX_PACKET_SIZE];
199198
while (!currentThread.isInterrupted() && runTunReadLoop(byteBuffer)) {}
200199
} catch (PVNClientException e) {
200+
XLog.tag(TAG).w("[id=%d] DISCONNECT REASON: VPN client error [code=%s, msg=%s]",
201+
connectionId, e.errorCode, e.errorMessage);
201202
sendExceptionToService(e);
202203
} catch (IOException ex) {
203-
XLog.tag(TAG).w("[id=" + connectionId + "] TUN interface closed: " + ex.getMessage());
204+
XLog.tag(TAG).w("[id=%d] DISCONNECT REASON: IO error — VPN likely revoked by system or another app [%s]",
205+
connectionId, ex.getMessage());
206+
pendingShutdownReason = FptnService.DISCONNECT_REASON_SYSTEM_REVOKED;
204207
} catch (WebSocketAlreadyShutdownException e) {
205-
XLog.tag(TAG).w("The websocket already shutdown", e);
208+
XLog.tag(TAG).w("[id=%d] DISCONNECT REASON: WebSocket already shut down", connectionId);
209+
pendingShutdownReason = FptnService.DISCONNECT_REASON_CLOSED_UNEXPECTEDLY;
206210
} catch (InterruptedException e) {
207-
XLog.tag(TAG).d("InterruptedException catch!", e);
211+
XLog.tag(TAG).i("[id=%d] DISCONNECT REASON: thread interrupted — explicit disconnect or service stopped", connectionId);
208212
} catch (Exception e) {
209-
XLog.tag(TAG).e("[id=" + connectionId + "] ERRORRR [wsStarted=" + webSocketClient.isStarted() + "]: " + e.getMessage());
213+
XLog.tag(TAG).e("[id=%d] DISCONNECT REASON: unexpected exception [type=%s, msg=%s, wsStarted=%b]",
214+
connectionId, e.getClass().getSimpleName(), e.getMessage(), webSocketClient.isStarted());
215+
pendingShutdownReason = FptnService.DISCONNECT_REASON_UNEXPECTED_ERROR;
210216
} finally {
211217
shutdown();
212218
}
@@ -258,11 +264,9 @@ private boolean runTunReadLoop(byte[] byteBuffer) throws InterruptedException, W
258264

259265
uploadRate.update(length);
260266
totalUploadBytes.addAndGet(length);
261-
if (!webSocketClient.isStarted()) {
262-
synchronized (webSocketLock) {
263-
webSocketLock.wait();
264-
}
265-
}
267+
// If WebSocket isn't started (reconnecting), send() silently drops the packet.
268+
// No blocking/waiting on the native layer's reconnect state; the read loop
269+
// keeps draining the TUN interface regardless.
266270
webSocketClient.send(byteBuffer, length);
267271
}
268272
} catch (IOException ex) {
@@ -346,7 +350,7 @@ public void shutdown() {
346350
webSocketClient.shutdown();
347351
scheduler.shutdown();
348352

349-
sendConnectionStateToService(ConnectionState.DISCONNECTED);
353+
sendConnectionStateToService(ConnectionState.DISCONNECTED, pendingShutdownReason);
350354
}
351355

352356
private void configureConnectionTimeSpeedScheduler() {
@@ -459,11 +463,6 @@ private void onConnectionOpen() {
459463
if (!currentThread.isInterrupted()) {
460464
sendConnectionStateToService(ConnectionState.CONNECTED);
461465
cancelReconnectTask();
462-
463-
// resume thread
464-
synchronized (webSocketLock) {
465-
webSocketLock.notify();
466-
}
467466
}
468467
}
469468

@@ -511,6 +510,7 @@ public void onConnectionFailure() {
511510
}
512511
if (!NetworkUtils.isOnline(connectivityManager)) {
513512
XLog.tag(TAG).i("[id=%d] No internet — suspending reconnect, entering WAITING_FOR_NETWORK", connectionId);
513+
cancelReconnectTask();
514514
service.enterWaitingForNetwork(serverEntity.getId());
515515
return;
516516
}
@@ -575,11 +575,15 @@ private void sendSpeedInfoAndDurationToService(String downloadSpeed, String uplo
575575
}
576576

577577
private void sendConnectionStateToService(ConnectionState connectionState) {
578-
service.updateConnectionState(connectionState, reconnectCount.get(), connectionId);
578+
service.updateConnectionState(connectionState, reconnectCount.get(), connectionId, null);
579+
}
580+
581+
private void sendConnectionStateToService(ConnectionState connectionState, String disconnectReason) {
582+
service.updateConnectionState(connectionState, reconnectCount.get(), connectionId, disconnectReason);
579583
}
580584

581585
private void sendConnectionStateToService(ConnectionState connectionState, int count) {
582-
service.updateConnectionState(connectionState, count, connectionId);
586+
service.updateConnectionState(connectionState, count, connectionId, null);
583587
}
584588

585589
private boolean isTunInterfaceValid(ParcelFileDescriptor vpnInterface) {

app/src/main/java/org/fptn/vpn/services/vpn/FptnService.java

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ public class FptnService extends VpnService {
9999
public static final String ACTION_BIND = "FptnService:BIND";
100100
public static final String FPTN_SERVICE_POWER_LOCK = "FptnService::POWER_LOCK";
101101

102+
public static final String DISCONNECT_REASON_SYSTEM_REVOKED = "reason:system_revoked";
103+
public static final String DISCONNECT_REASON_CLOSED_UNEXPECTEDLY = "reason:closed_unexpectedly";
104+
public static final String DISCONNECT_REASON_UNEXPECTED_ERROR = "reason:unexpected_error";
105+
102106
private final AtomicReference<FptnConnection> activeConnection = new AtomicReference<>();
103107
private final AtomicInteger nextConnectionId = new AtomicInteger(1);
104108
private final AtomicInteger remainingFallbackBudget = new AtomicInteger(0);
@@ -117,6 +121,7 @@ public class FptnService extends VpnService {
117121

118122
private ConnectivityManager.NetworkCallback networkCallback;
119123
private ConnectivityManager connectivityManager;
124+
private volatile String pendingRevokeReason = null;
120125

121126
@Getter
122127
private final MutableLiveData<FptnServiceState> serviceStateMutableLiveData = new MutableLiveData<>(FptnServiceState.INITIAL);
@@ -180,7 +185,8 @@ public void disconnectSilently(int senderConnectionId) {
180185
if (current == null || current.getConnectionId() != senderConnectionId) {
181186
return;
182187
}
183-
executorService.submit(() -> disconnect(null));
188+
XLog.tag(TAG).w("DISCONNECT REASON: silent disconnect requested [connectionId=%d] — TUN gone or foreign VPN detected", senderConnectionId);
189+
executorService.submit(() -> disconnect(null, getString(R.string.disconnect_reason_vpn_conflict)));
184190
}
185191

186192
public void sendExceptionToService(PVNClientException exception, int senderConnectionId) {
@@ -203,6 +209,10 @@ public void sendExceptionToService(PVNClientException exception, int senderConne
203209
}
204210

205211
public void updateConnectionState(ConnectionState connectionState, int reconnectionCount, int senderConnectionId) {
212+
updateConnectionState(connectionState, reconnectionCount, senderConnectionId, null);
213+
}
214+
215+
public void updateConnectionState(ConnectionState connectionState, int reconnectionCount, int senderConnectionId, String disconnectReason) {
206216
if (connectionState == ConnectionState.DISCONNECTED) {
207217
FptnConnection current = activeConnection.get();
208218
if (current != null && current.getConnectionId() != senderConnectionId) {
@@ -211,7 +221,7 @@ public void updateConnectionState(ConnectionState connectionState, int reconnect
211221
return;
212222
}
213223
}
214-
switchState(connectionState, reconnectionCount);
224+
switchState(connectionState, reconnectionCount, disconnectReason);
215225
}
216226

217227
/* Static methods to start/stop service */
@@ -366,7 +376,8 @@ private ServerEntity getSelectedServer() throws ExecutionException, InterruptedE
366376
@Override
367377
public void onRevoke() {
368378
XLog.tag(TAG).i("VPN permission revoked — another VPN took over, disconnecting");
369-
executorService.submit(() -> disconnect());
379+
pendingRevokeReason = getString(R.string.disconnect_reason_vpn_revoked);
380+
executorService.submit(() -> disconnect(null, pendingRevokeReason));
370381
}
371382

372383
@Override
@@ -378,6 +389,14 @@ public void onDestroy() {
378389
FptnTileService.getServiceStateMutableLiveData().setValue(ConnectionState.DISCONNECTED);
379390
notifyTileListeningState();
380391

392+
// If revoked, post disconnect reason to UI synchronously before observer is removed
393+
if (pendingRevokeReason != null) {
394+
serviceStateMutableLiveData.setValue(FptnServiceState.builder()
395+
.connectionState(ConnectionState.DISCONNECTED)
396+
.disconnectReason(pendingRevokeReason)
397+
.build());
398+
}
399+
381400
disconnect();
382401

383402
if (serviceStateObserver != null) {
@@ -396,7 +415,7 @@ public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapa
396415
FptnConnection currentConnection = activeConnection.get();
397416
ConnectionState connectionState = Optional.ofNullable(serviceStateMutableLiveData.getValue())
398417
.map(FptnServiceState::getConnectionState).orElse(null);
399-
if (currentConnection != null && (connectionState == ConnectionState.CONNECTED || connectionState == ConnectionState.RECONNECTING)) {
418+
if (currentConnection != null && connectionState == ConnectionState.CONNECTED) {
400419
Network activeNetwork = connectivityManager.getActiveNetwork();
401420
NetworkCapabilities activeNetworkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork);
402421
if (activeNetworkCapabilities != null && activeNetworkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
@@ -553,12 +572,16 @@ private void startConnectionAttempt(int initialServerId) {
553572
}
554573

555574
private void switchState(ConnectionState connectionState, int reconnectCount) {
575+
switchState(connectionState, reconnectCount, null);
576+
}
577+
578+
private void switchState(ConnectionState connectionState, int reconnectCount, String disconnectReason) {
556579
XLog.tag(TAG).i("State transition -> %s%s", connectionState,
557580
reconnectCount > 0 ? " [attempt " + reconnectCount + "]" : "");
558581
switch (connectionState) {
559582
case DISCONNECTED -> {
560583
if (activeConnection.get() != null) {
561-
disconnect();
584+
disconnect(null, disconnectReason);
562585
}
563586
}
564587
case CONNECTING -> setConnectionState(ConnectionState.CONNECTING, null);
@@ -590,9 +613,14 @@ public String getActionConnectServerInfo() {
590613
}
591614

592615
private void setConnectionState(ConnectionState connectionState, PVNClientException exception) {
616+
setConnectionState(connectionState, exception, null);
617+
}
618+
619+
private void setConnectionState(ConnectionState connectionState, PVNClientException exception, String disconnectReason) {
593620
serviceStateMutableLiveData.postValue(FptnServiceState.builder()
594621
.connectionState(connectionState)
595622
.exception(exception)
623+
.disconnectReason(disconnectReason)
596624
.build());
597625
}
598626

@@ -827,15 +855,31 @@ private void setActiveConnection(FptnConnection connection) {
827855
}
828856

829857
private void disconnect() {
830-
//disconnect without exception
831-
disconnect(null);
858+
disconnect(null, null);
832859
}
833860

834861
private void disconnect(PVNClientException exception) {
835-
if (exception == null) {
836-
XLog.tag(TAG).i("Disconnecting [reason=user]");
862+
disconnect(exception, null);
863+
}
864+
865+
private String resolveDisconnectReason(String key) {
866+
if (key == null) return null;
867+
switch (key) {
868+
case DISCONNECT_REASON_SYSTEM_REVOKED: return getString(R.string.disconnect_reason_system_revoked);
869+
case DISCONNECT_REASON_CLOSED_UNEXPECTEDLY: return getString(R.string.disconnect_reason_closed_unexpectedly);
870+
case DISCONNECT_REASON_UNEXPECTED_ERROR: return getString(R.string.disconnect_reason_unexpected_error);
871+
default: return key;
872+
}
873+
}
874+
875+
private void disconnect(PVNClientException exception, String disconnectReasonKey) {
876+
String disconnectReason = resolveDisconnectReason(disconnectReasonKey);
877+
if (exception == null && disconnectReason == null) {
878+
XLog.tag(TAG).w("DISCONNECT REASON: user action");
879+
} else if (disconnectReason != null) {
880+
XLog.tag(TAG).w("DISCONNECT REASON: %s", disconnectReason);
837881
} else {
838-
XLog.tag(TAG).w("Disconnecting [reason=error, code=%s, message=%s]",
882+
XLog.tag(TAG).w("DISCONNECT REASON: error [code=%s, message=%s]",
839883
exception.errorCode, exception.errorMessage);
840884
}
841885
// stop and null existed connection
@@ -845,7 +889,7 @@ private void disconnect(PVNClientException exception) {
845889
// sometimes need to remove notification explicitly
846890
removeForegroundNotification();
847891
//send to UI activity that state is disconnected.
848-
setConnectionState(ConnectionState.DISCONNECTED, exception);
892+
setConnectionState(ConnectionState.DISCONNECTED, exception, disconnectReason);
849893

850894
if (exception != null) {
851895
ErrorCode errorCode = exception.errorCode;
@@ -871,7 +915,7 @@ private void disconnect(PVNClientException exception) {
871915
resetSelectedServer();
872916

873917
//send to UI activity that state is disconnected.
874-
setConnectionState(ConnectionState.DISCONNECTED, exception);
918+
setConnectionState(ConnectionState.DISCONNECTED, exception, disconnectReason);
875919
} catch (ExecutionException | InterruptedException e) {
876920
XLog.tag(TAG).e("Failed to reset selected server: %s", e.getMessage());
877921
}

app/src/main/java/org/fptn/vpn/services/vpn/FptnServiceState.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class FptnServiceState {
3434
private final ConnectionState connectionState;
3535
private final PVNClientException exception;
3636
private final String serverInfo;
37+
private final String disconnectReason;
3738

3839
public static final FptnServiceState INITIAL = FptnServiceState.builder()
3940
.connectionState(ConnectionState.DISCONNECTED)

app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,8 @@ public void send(byte[] bytes, long length) {
154154
&& nativeWebSocketClient.isStarted()
155155
&& length > 0) {
156156
nativeWebSocketClient.send(bytes, length);
157-
} else {
158-
throw new RuntimeException("nativeWebSocketClient is null or not started");
159157
}
158+
// silently drop otherwise (reconnecting/not ready) — no exception, no log spam
160159
}
161160

162161
public synchronized void shutdown() {

app/src/main/java/org/fptn/vpn/views/home/HomeActivity.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,8 +375,8 @@ public void onClickToStartStop(View v) {
375375
return;
376376
}
377377

378-
// Request required permissions if not yet granted — checked on every connect attempt
379-
if (!PermissionsUtils.isAllOptionalPermissionsGranted(this)) {
378+
// Ask for optional permissions until user grants them; once granted, never ask again
379+
if (!SharedPrefUtils.isPermissionsRequested(this)) {
380380
startStopButton.setChecked(false);
381381
requestRequiredPermissions();
382382
// proceedToVpnConnect() is called from the dialog callbacks — not here
@@ -487,6 +487,9 @@ private void requestAddTileService() {
487487
XLog.tag(TAG).w("System permission denied via Settings");
488488
}
489489
if (requestedPermissions.decrementAndGet() == 0) {
490+
if (PermissionsUtils.isAllOptionalPermissionsGranted(this)) {
491+
SharedPrefUtils.savePermissionsRequested(this, true);
492+
}
490493
proceedToVpnConnect();
491494
}
492495
}
@@ -510,6 +513,11 @@ private void requestRequiredPermissions() {
510513
requestedPermissions.incrementAndGet();
511514
startActivityWithSettings(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS);
512515
}
516+
// Nothing to open — permissions already granted, save and proceed
517+
if (requestedPermissions.get() == 0) {
518+
SharedPrefUtils.savePermissionsRequested(this, true);
519+
proceedToVpnConnect();
520+
}
513521
})
514522
.setNegativeButton(getString(R.string.deny), (dialog, which) -> {
515523
XLog.tag(TAG).w("Optional permissions denied by user — continuing without them");

app/src/main/java/org/fptn/vpn/views/home/HomeActivityViewModel.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,11 @@ public HomeActivityViewModel(@NonNull Application application) {
111111
ConnectionState connectionState = fptnServiceState.getConnectionState();
112112
switch (connectionState) {
113113
case DISCONNECTED -> {
114-
statusTextLiveData.postValue(getApplication().getString(R.string.disconnected));
114+
String reason = fptnServiceState.getDisconnectReason();
115+
String label = (reason != null && !reason.isEmpty())
116+
? reason
117+
: getApplication().getString(R.string.disconnected);
118+
statusTextLiveData.postValue(label);
115119
resetErrorMessage();
116120
refreshServerListFromDB();
117121
}

0 commit comments

Comments
 (0)