Skip to content

Commit a23ced2

Browse files
committed
[bugfix] xiaomi
1 parent baac9bf commit a23ced2

8 files changed

Lines changed: 101 additions & 6 deletions

File tree

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,6 @@ public IBinder onBind(Intent intent) {
182182
}
183183
/* binder part END */
184184

185-
186185
public void updateSpeedInfo(String downloadSpeed, String uploadSpeed, long duration, long totalDownload, long totalUpload, long downloadBps, long uploadBps) {
187186
if (serviceStateMutableLiveData.getValue().getConnectionState() == ConnectionState.CONNECTED) {
188187
if (SharedPrefUtils.getShowSpeedInNotification(getApplication())) {
@@ -419,6 +418,12 @@ public int onStartCommand(Intent intent, int flags, int startId) {
419418

420419
if (!NetworkUtils.isOnline(connectivityManager)) {
421420
XLog.tag(TAG).i("No internet — entering WAITING_FOR_NETWORK state");
421+
// This IS the "reconnect on network loss" scenario the attempts setting is for:
422+
// when the network returns half-alive, the first failed scan must go through the
423+
// recovery cycle (retry/scan per the configured budget), not die with a terminal
424+
// "all servers unreachable" — with restoringSession=false it did exactly that.
425+
restoringSession = true;
426+
remainingFallbackBudget.set(SharedPrefUtils.getReconnectAttemptsCount(this));
422427
pendingServerId = intent.getIntExtra(SELECTED_SERVER, SELECTED_SERVER_ID_AUTO);
423428
updateNotificationWithMessage(getString(R.string.waiting_for_network), "");
424429
setConnectionState(ConnectionState.WAITING_FOR_NETWORK, null);

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

Lines changed: 35 additions & 5 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;
@@ -139,21 +141,49 @@ public static boolean checkNotificationEnabled(Context context) {
139141
}
140142

141143
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-
}
147144
boolean isGranted = false;
148145
PowerManager powerManager = (PowerManager) context.getSystemService(POWER_SERVICE);
149146
if (powerManager != null) {
150147
isGranted = powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
151148
}
149+
// MIUI (Xiaomi/Redmi/POCO all report MANUFACTURER=xiaomi) runs its own battery manager;
150+
// isIgnoringBatteryOptimizations() stays false even after the user grants the exemption.
151+
// Trusting it would nag on every connect and never let the first-run flow complete, so once
152+
// we have sent the user to the system dialog we treat the exemption as handled here.
153+
if (!isGranted && isXiaomi() && SharedPrefUtils.isBatteryOptimizationRequested(context)) {
154+
isGranted = true;
155+
}
152156
XLog.tag(TAG).i("Battery optimization exemption [granted=%b, manufacturer=%s, brand=%s, model=%s]",
153157
isGranted, Build.MANUFACTURER, Build.BRAND, Build.MODEL);
154158
return isGranted;
155159
}
156160

161+
public static boolean isXiaomi() {
162+
return "xiaomi".equalsIgnoreCase(Build.MANUFACTURER)
163+
|| "xiaomi".equalsIgnoreCase(Build.BRAND)
164+
|| "redmi".equalsIgnoreCase(Build.BRAND)
165+
|| "poco".equalsIgnoreCase(Build.BRAND);
166+
}
167+
168+
/**
169+
* Opens the app-details screen so the user can reach the MIUI background / battery controls
170+
* (the standard battery-optimization exemption is not enough on MIUI).
171+
*
172+
* @return true if some settings screen was launched.
173+
*/
174+
public static boolean openMiuiBackgroundSettings(Context context) {
175+
try {
176+
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
177+
intent.setData(Uri.parse("package:" + context.getPackageName()));
178+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
179+
context.startActivity(intent);
180+
return true;
181+
} catch (Exception e) {
182+
XLog.tag(TAG).e("Failed to open settings for MIUI guidance: %s", e.getMessage());
183+
return false;
184+
}
185+
}
186+
157187
public static boolean checkBackgroundDataTransferRestrictions(Context context) {
158188
boolean isGranted = false;
159189
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(CONNECTIVITY_SERVICE);

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,26 @@ public static void savePermissionsRequested(Context context, boolean requested)
6666
sharedPreferences.edit().putBoolean(Constants.PERMISSIONS_REQUESTED_SHARED_PREF_KEY, requested).apply();
6767
}
6868

69+
public static boolean isBatteryOptimizationRequested(Context context) {
70+
SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE);
71+
return sharedPreferences.getBoolean(Constants.BATTERY_OPTIMIZATION_REQUESTED_SHARED_PREF_KEY, false);
72+
}
73+
74+
public static void saveBatteryOptimizationRequested(Context context, boolean requested) {
75+
SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE);
76+
sharedPreferences.edit().putBoolean(Constants.BATTERY_OPTIMIZATION_REQUESTED_SHARED_PREF_KEY, requested).apply();
77+
}
78+
79+
public static boolean isXiaomiBackgroundHintShown(Context context) {
80+
SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE);
81+
return sharedPreferences.getBoolean(Constants.XIAOMI_BACKGROUND_HINT_SHOWN_SHARED_PREF_KEY, false);
82+
}
83+
84+
public static void saveXiaomiBackgroundHintShown(Context context, boolean shown) {
85+
SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE);
86+
sharedPreferences.edit().putBoolean(Constants.XIAOMI_BACKGROUND_HINT_SHOWN_SHARED_PREF_KEY, shown).apply();
87+
}
88+
6989
/* QUICK SETTINGS TILE */
7090
public static boolean isQuickSettingsTileRequested(Context context) {
7191
SharedPreferences sharedPreferences = context.getSharedPreferences(Constants.APPLICATION_SHARED_PREFERENCES, Context.MODE_PRIVATE);

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,14 @@ public void onClickToStartStop(View v) {
392392
}
393393

394394
private void proceedToVpnConnect() {
395+
// MIUI kills background apps regardless of the battery exemption, dropping the VPN (and
396+
// leaking the real IP) while the screen is off. Guide Xiaomi users to the background /
397+
// battery settings once before the first connect.
398+
if (PermissionsUtils.isXiaomi() && !SharedPrefUtils.isXiaomiBackgroundHintShown(this)) {
399+
SharedPrefUtils.saveXiaomiBackgroundHintShown(this, true);
400+
showXiaomiBackgroundHintDialog();
401+
return;
402+
}
395403
if (PermissionsUtils.isAlwaysOnVpnEnabledByAnotherApp(this)) {
396404
startStopButton.setChecked(false);
397405
showVpnSwitchDialog();
@@ -400,6 +408,21 @@ private void proceedToVpnConnect() {
400408
connectVpn();
401409
}
402410

411+
private void showXiaomiBackgroundHintDialog() {
412+
new AlertDialog.Builder(this)
413+
.setTitle(R.string.xiaomi_background_hint_title)
414+
.setMessage(R.string.xiaomi_background_hint_text)
415+
.setCancelable(false)
416+
.setPositiveButton(R.string.xiaomi_background_hint_open, (d, w) -> {
417+
// User is heading into system settings — don't auto-connect; they can tap
418+
// connect again when they return (the hint won't show a second time).
419+
startStopButton.setChecked(false);
420+
PermissionsUtils.openMiuiBackgroundSettings(this);
421+
})
422+
.setNegativeButton(R.string.xiaomi_background_hint_later, (d, w) -> proceedToVpnConnect())
423+
.show();
424+
}
425+
403426
private void connectVpn() {
404427
Intent intent = VpnService.prepare(this);
405428
if (intent != null) {
@@ -506,6 +529,9 @@ private void requestRequiredPermissions() {
506529
if (!PermissionsUtils.checkBatteryOptimizations(this)) {
507530
//Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
508531
requestedPermissions.incrementAndGet();
532+
// On MIUI the OS never reports the exemption back — record that we asked so
533+
// the check can settle instead of re-prompting on every connect.
534+
SharedPrefUtils.saveBatteryOptimizationRequested(this, true);
509535
startActivityWithSettings(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
510536
}
511537
// Background data transfer restriction permission

app/src/main/java/org/fptn/vpn/views/settings/SettingsActivity.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646

4747
import org.fptn.vpn.R;
4848
import org.fptn.vpn.utils.PermissionsUtils;
49+
import org.fptn.vpn.utils.SharedPrefUtils;
4950
import org.fptn.vpn.views.CustomBottomNavigationListener;
5051
import org.fptn.vpn.views.experimentalsettings.ExperimentalSettingsActivity;
5152
import org.fptn.vpn.views.log.LogsActivity;
@@ -178,6 +179,9 @@ private void requestBatteryOptimisationPermission() {
178179
.setPositiveButton(getString(R.string.grant), (d, w) -> {
179180
@SuppressLint("BatteryLife") Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
180181
intent.setData(Uri.parse("package:" + getPackageName()));
182+
// On MIUI the OS never reports the exemption back — record that we asked so the
183+
// toggle can settle to granted instead of always showing off.
184+
SharedPrefUtils.saveBatteryOptimizationRequested(this, true);
181185
startActivity(intent);
182186
})
183187
.setNegativeButton(getString(R.string.deny), (dialog, which) -> {

app/src/main/res/values-ru-rRU/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ https://play.google.com/store/apps/details?id=org.fptn.vpn
127127
<string name="warning">Внимание</string>
128128
<string name="battery_optimization_request_dialog_title">Откл оптимизацию батареи</string>
129129
<string name="battery_optimization_request_dialog_text">Для стабильного подключения предоставьте разрешение на отключение оптимизации энергопотребления для этого приложения</string>
130+
<string name="xiaomi_background_hint_title">Разрешите работу в фоне</string>
131+
<string name="xiaomi_background_hint_text">MIUI останавливает приложения в фоне — из-за этого VPN отваливается при выключенном экране, а реальный IP может утечь. Чтобы соединение не рвалось, выберите «Экономия батареи» → «Без ограничений» для этого приложения.</string>
132+
<string name="xiaomi_background_hint_open">Открыть настройки</string>
133+
<string name="xiaomi_background_hint_later">Продолжить</string>
130134

131135
<string name="required_permissions">Необходимые разрешения:</string>
132136
<string name="denied_text">ОТКЛОНЕНО</string>

app/src/main/res/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ https://play.google.com/store/apps/details?id=org.fptn.vpn
138138

139139
<string name="battery_optimization_request_dialog_title">Disable battery optimization</string>
140140
<string name="battery_optimization_request_dialog_text">For a stable connection, please grant permission to disable battery optimization for this app</string>
141+
<string name="xiaomi_background_hint_title">Allow background operation</string>
142+
<string name="xiaomi_background_hint_text">MIUI stops apps in the background, so the VPN drops while the screen is off and your real IP can leak. To keep the connection alive, set \"Battery saver\" to \"No restrictions\" for this app.</string>
143+
<string name="xiaomi_background_hint_open">Open settings</string>
144+
<string name="xiaomi_background_hint_later">Continue</string>
141145

142146
<string name="required_permissions">Required permissions:</string>
143147
<string name="granted_text">GRANTED</string>

core/common/src/main/kotlin/org/fptn/vpn/core/common/Constants.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ object Constants {
5252
const val RESET_SELECTED_SERVER_ON_EXCEPTION_PREF_KEY: String = "RESET_SELECTED_SERVER_ON_EXCEPTION_PREF_KEY"
5353
const val APPLICATION_SHARED_PREFERENCES = "fptnvpn-shared-preferences"
5454
const val PERMISSIONS_REQUESTED_SHARED_PREF_KEY: String = "permissions_requested_previously"
55+
const val BATTERY_OPTIMIZATION_REQUESTED_SHARED_PREF_KEY: String = "battery_optimization_requested"
56+
const val XIAOMI_BACKGROUND_HINT_SHOWN_SHARED_PREF_KEY: String = "xiaomi_background_hint_shown"
5557
const val RECONNECT_ON_CHANGE_IP_ENABLED_SHARED_PREF_KEY: String = "RECONNECT_ON_CHANGE_IP_ENABLED_V2"
5658
const val RECONNECT_ON_CHANGE_NETWORK_TYPE_ENABLED_SHARED_PREF_KEY: String =
5759
"RECONNECT_ON_CHANGE_NETWORK_TYPE_ENABLED"

0 commit comments

Comments
 (0)