Skip to content

Commit 73ed0e8

Browse files
authored
[bugfix] wait for validated network on reconnect (#351)
* [bugfix] wait for validated network on reconnect * [bugfix] xiaomi * [feature] background-setup checklist for reliable background operation
1 parent 95e728b commit 73ed0e8

17 files changed

Lines changed: 695 additions & 154 deletions

File tree

app/src/main/cpp/libs/fptn

Submodule fptn updated from ef5730a to dd2a120

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

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

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

7070
std::thread th_;
7171
mutable std::mutex mutex_;

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

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,7 @@
3232
import android.content.pm.PackageManager;
3333
import android.net.ConnectivityManager;
3434
import android.net.IpPrefix;
35-
import android.net.Network;
36-
import android.net.NetworkCapabilities;
3735
import android.net.VpnService;
38-
import android.content.Context;
3936
import android.os.Build;
4037
import android.os.ParcelFileDescriptor;
4138

@@ -154,7 +151,8 @@ public FptnConnection(final FptnService service,
154151
final String preFetchedToken,
155152
final ConnectivityManager connectivityManager,
156153
final AdBlocker adBlocker,
157-
final String customDnsIpv4) {
154+
final String customDnsIpv4,
155+
final DnsServers preFetchedDnsServers) {
158156
this.service = service;
159157
this.connectionId = connectionId;
160158
this.serverEntity = serverEntity;
@@ -179,7 +177,8 @@ public FptnConnection(final FptnService service,
179177
sniHostName,
180178
censorshipStrategy,
181179
sniSpoofingMode,
182-
preFetchedToken
180+
preFetchedToken,
181+
preFetchedDnsServers
183182
);
184183
}
185184

@@ -371,6 +370,7 @@ private void configureConnectionTimeSpeedScheduler() {
371370

372371
private void configureAddressesAndRoutes(VpnService.Builder builder) throws UnknownHostException, PVNClientException {
373372
final DnsServers dnsServers = webSocketClient.getDnsServers();
373+
service.cacheDnsServers(serverEntity.getId(), dnsServers);
374374

375375
// Custom DNS (added first so it takes priority)
376376
if (customDnsIpv4 != null && !customDnsIpv4.isEmpty()) {
@@ -492,10 +492,8 @@ public void onConnectionFailure() {
492492
webSocketClient.isStarted(),
493493
tunValid,
494494
onFailureScheduledTask != null && !onFailureScheduledTask.isCancelled());
495-
boolean otherVpnActive = isOtherVpnActive();
496-
XLog.tag(TAG).w("[id=%d] VPN state check [tunValid=%b, otherVpnActive=%b]", connectionId, tunValid, otherVpnActive);
497-
if (!tunValid || otherVpnActive) {
498-
XLog.tag(TAG).i("[id=%d] Disconnecting silently [tunValid=%b, otherVpnActive=%b]", connectionId, tunValid, otherVpnActive);
495+
if (!tunValid) {
496+
XLog.tag(TAG).i("[id=%d] Disconnecting silently [tunValid=%b]", connectionId, tunValid);
499497
service.disconnectSilently(connectionId);
500498
return;
501499
}
@@ -590,30 +588,6 @@ private boolean isTunInterfaceValid(ParcelFileDescriptor vpnInterface) {
590588
return vpnInterface != null && vpnInterface.getFileDescriptor() != null && vpnInterface.getFileDescriptor().valid();
591589
}
592590

593-
private boolean isOtherVpnActive() {
594-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
595-
return false;
596-
}
597-
try {
598-
ConnectivityManager cm = (ConnectivityManager) service.getSystemService(Context.CONNECTIVITY_SERVICE);
599-
if (cm == null) return false;
600-
int myUid = service.getApplicationInfo().uid;
601-
for (Network network : cm.getAllNetworks()) {
602-
NetworkCapabilities caps = cm.getNetworkCapabilities(network);
603-
if (caps != null && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
604-
&& caps.getOwnerUid() != myUid) {
605-
return true;
606-
}
607-
}
608-
} catch (NoSuchMethodError e) {
609-
// Samsung Android 10 ships without getOwnerUid() despite API level 29
610-
XLog.tag(TAG).w("[id=%d] getOwnerUid() not available on this device", connectionId);
611-
} catch (Exception e) {
612-
XLog.tag(TAG).w("[id=%d] isOtherVpnActive check failed: %s", connectionId, e.getMessage());
613-
}
614-
return false;
615-
}
616-
617591
public void onNetworkChanged() {
618592
XLog.tag(TAG).i("[id=%d] Network changed — forcing clean reconnect [wsStarted=%b]",
619593
connectionId, webSocketClient.isStarted());

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

Lines changed: 223 additions & 14 deletions
Large diffs are not rendered by default.

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public class WebSocketClientWrapper {
5555

5656
private NativeWebSocketClientImpl nativeWebSocketClient;
5757
private String cachedAccessToken = null;
58+
private DnsServers cachedDnsServers = null;
5859

5960
@Getter
6061
private boolean shutdown = false;
@@ -68,7 +69,8 @@ public WebSocketClientWrapper(ServerEntity serverEntity,
6869
String sniHostName,
6970
BypassCensorshipMethod censorshipStrategy,
7071
SniSpoofingMode sniSpoofingMode,
71-
String preFetchedToken) {
72+
String preFetchedToken,
73+
DnsServers preFetchedDnsServers) {
7274
this.serverEntity = serverEntity;
7375
this.tunAddressIPv4 = tunAddressIPv4;
7476
this.tunAddressIPv6 = tunAddressIPv6;
@@ -82,6 +84,7 @@ public WebSocketClientWrapper(ServerEntity serverEntity,
8284
this.sniSpoofingMode = sniSpoofingMode;
8385

8486
this.cachedAccessToken = preFetchedToken;
87+
this.cachedDnsServers = preFetchedDnsServers;
8588

8689
this.nativeHttpsClient = new NativeHttpsClientImpl(
8790
serverEntity.getHost(),
@@ -209,6 +212,10 @@ private String getAccessToken() throws PVNClientException {
209212
}
210213

211214
public DnsServers getDnsServers() throws PVNClientException {
215+
if (cachedDnsServers != null) {
216+
XLog.d(getTag(), "Re-using cached DNS servers (skipping /api/v1/dns request)");
217+
return cachedDnsServers;
218+
}
212219
int maxAttempts = 7;
213220
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
214221
if (isShutdown() || Thread.currentThread().isInterrupted()) {
@@ -218,6 +225,7 @@ public DnsServers getDnsServers() throws PVNClientException {
218225
if (response != null && response.code == 200) {
219226
DnsServers dnsServers = new Gson().fromJson(response.body, DnsServers.class);
220227
XLog.tag(getTag()).i("DNS servers received: %s", dnsServers.toString());
228+
cachedDnsServers = dnsServers;
221229
return dnsServers;
222230
}
223231
XLog.tag(getTag()).w("DNS server request failed [attempt=%d/%d]", attempt, maxAttempts);

app/src/main/java/org/fptn/vpn/utils/NotificationUtils.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public static void configureNotificationChannels(Context context) {
4141
Constants.MAIN_NOTIFICATION_CHANNEL_VERSION_NUM,
4242
Constants.MAIN_NOTIFICATION_CHANNEL_GROUP_ID,
4343
context.getString(R.string.notification_group_name),
44-
NotificationManager.IMPORTANCE_LOW, true);
44+
NotificationManager.IMPORTANCE_DEFAULT, true);
4545

4646
// create error notification channel
4747
createOrUpdateNotificationChannel(context, notificationManager,
@@ -51,7 +51,7 @@ public static void configureNotificationChannels(Context context) {
5151
Constants.ERROR_NOTIFICATION_CHANNEL_VERSION_NUM,
5252
Constants.ERROR_NOTIFICATION_CHANNEL_GROUP_ID,
5353
context.getString(R.string.errors_notification_group_name),
54-
NotificationManager.IMPORTANCE_LOW, true);
54+
NotificationManager.IMPORTANCE_HIGH, true);
5555

5656
// add sni checker notification channel
5757
createOrUpdateNotificationChannel(context, notificationManager,

app/src/main/java/org/fptn/vpn/utils/PermissionsUtils.java

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
import android.app.NotificationChannel;
2828
import android.app.NotificationManager;
2929
import android.content.Context;
30+
import android.content.Intent;
3031
import android.content.pm.PackageManager;
3132
import android.net.ConnectivityManager;
3233
import android.net.Network;
3334
import android.net.NetworkCapabilities;
35+
import android.net.Uri;
3436
import android.os.Build;
3537
import android.os.PowerManager;
3638
import android.provider.Settings;
@@ -113,10 +115,6 @@ public static boolean isAlwaysOnVpnEnabledByAnotherApp(Context context) {
113115
return false;
114116
}
115117

116-
public static boolean isAllOptionalPermissionsGranted(Context context) {
117-
return checkBackgroundDataTransferRestrictions(context) && checkBatteryOptimizations(context);
118-
}
119-
120118
public static boolean checkNotificationEnabled(Context context) {
121119
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
122120
if (!notificationManager.areNotificationsEnabled()) {
@@ -139,21 +137,71 @@ public static boolean checkNotificationEnabled(Context context) {
139137
}
140138

141139
public static boolean checkBatteryOptimizations(Context context) {
142-
if ("xiaomi".equalsIgnoreCase(Build.MANUFACTURER)) {
143-
XLog.tag(TAG).i("Battery optimization check skipped [manufacturer=%s, brand=%s, model=%s]",
144-
Build.MANUFACTURER, Build.BRAND, Build.MODEL);
145-
return true;
146-
}
147140
boolean isGranted = false;
148141
PowerManager powerManager = (PowerManager) context.getSystemService(POWER_SERVICE);
149142
if (powerManager != null) {
150143
isGranted = powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
151144
}
145+
// MIUI (Xiaomi/Redmi/POCO all report MANUFACTURER=xiaomi) runs its own battery manager;
146+
// isIgnoringBatteryOptimizations() stays false even after the user grants the exemption.
147+
// Trusting it would nag on every connect and never let the first-run flow complete, so once
148+
// we have sent the user to the system dialog we treat the exemption as handled here.
149+
if (!isGranted && isXiaomi() && SharedPrefUtils.isBatteryOptimizationRequested(context)) {
150+
isGranted = true;
151+
}
152152
XLog.tag(TAG).i("Battery optimization exemption [granted=%b, manufacturer=%s, brand=%s, model=%s]",
153153
isGranted, Build.MANUFACTURER, Build.BRAND, Build.MODEL);
154154
return isGranted;
155155
}
156156

157+
public static boolean isXiaomi() {
158+
return "xiaomi".equalsIgnoreCase(Build.MANUFACTURER)
159+
|| "xiaomi".equalsIgnoreCase(Build.BRAND)
160+
|| "redmi".equalsIgnoreCase(Build.BRAND)
161+
|| "poco".equalsIgnoreCase(Build.BRAND);
162+
}
163+
164+
/**
165+
* Opens the app-details screen so the user can reach the MIUI background / battery controls
166+
* (the standard battery-optimization exemption is not enough on MIUI).
167+
*
168+
* @return true if some settings screen was launched.
169+
*/
170+
public static boolean openMiuiBackgroundSettings(Context context) {
171+
try {
172+
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
173+
intent.setData(Uri.parse("package:" + context.getPackageName()));
174+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
175+
context.startActivity(intent);
176+
return true;
177+
} catch (Exception e) {
178+
XLog.tag(TAG).e("Failed to open settings for MIUI guidance: %s", e.getMessage());
179+
return false;
180+
}
181+
}
182+
183+
/**
184+
* Launches the MIUI "Security" app so the user can reach the "Locked apps" (pin in memory)
185+
* screen. Deep-linking straight to that screen is not a stable public API across HyperOS
186+
* versions, so we open the app by package and fall back to the app-details screen.
187+
*
188+
* @return true if some screen was launched.
189+
*/
190+
public static boolean openMiuiSecurityApp(Context context) {
191+
try {
192+
Intent intent = context.getPackageManager()
193+
.getLaunchIntentForPackage("com.miui.securitycenter");
194+
if (intent != null) {
195+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
196+
context.startActivity(intent);
197+
return true;
198+
}
199+
} catch (Exception e) {
200+
XLog.tag(TAG).w("Failed to open MIUI Security app: %s", e.getMessage());
201+
}
202+
return openMiuiBackgroundSettings(context);
203+
}
204+
157205
public static boolean checkBackgroundDataTransferRestrictions(Context context) {
158206
boolean isGranted = false;
159207
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(CONNECTIVITY_SERVICE);

app/src/main/java/org/fptn/vpn/utils/SharedPrefUtils.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,26 @@ public static void saveNotificationChannelVersion(Context context, String channe
5656
sharedPreferences.edit().putInt(channelVersionTag, version).apply();
5757
}
5858

59-
public static boolean isPermissionsRequested(Context context) {
59+
public static boolean isBatteryOptimizationRequested(Context context) {
6060
SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE);
61-
return sharedPreferences.getBoolean(Constants.PERMISSIONS_REQUESTED_SHARED_PREF_KEY, false);
61+
return sharedPreferences.getBoolean(Constants.BATTERY_OPTIMIZATION_REQUESTED_SHARED_PREF_KEY, false);
6262
}
6363

64-
public static void savePermissionsRequested(Context context, boolean requested) {
64+
public static void saveBatteryOptimizationRequested(Context context, boolean requested) {
6565
SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE);
66-
sharedPreferences.edit().putBoolean(Constants.PERMISSIONS_REQUESTED_SHARED_PREF_KEY, requested).apply();
66+
sharedPreferences.edit().putBoolean(Constants.BATTERY_OPTIMIZATION_REQUESTED_SHARED_PREF_KEY, requested).apply();
67+
}
68+
69+
// Xiaomi "lock in Security" can't be read back from the OS, so we remember once the user has
70+
// opened it and treat the step as handled from then on.
71+
public static boolean isXiaomiPinDone(Context context) {
72+
SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE);
73+
return sharedPreferences.getBoolean(Constants.XIAOMI_PIN_DONE_SHARED_PREF_KEY, false);
74+
}
75+
76+
public static void saveXiaomiPinDone(Context context, boolean done) {
77+
SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE);
78+
sharedPreferences.edit().putBoolean(Constants.XIAOMI_PIN_DONE_SHARED_PREF_KEY, done).apply();
6779
}
6880

6981
/* QUICK SETTINGS TILE */

0 commit comments

Comments
 (0)