@@ -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 }
0 commit comments