omnAPP/docs/bugfix_android_vpn_permission_ui_sync.md
Rust d87c58ac26
Some checks failed
Build Windows / build (push) Has been cancelled
修复安卓bug
2025-10-03 15:20:49 +08:00

201 lines
6.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Android VPN 权限弹窗关闭后 UI 状态不同步问题修复
## 问题描述
在 Android 平台上,当用户第一次打开 App 后点击连接按钮时,系统会弹出 VPN 权限请求弹窗。如果用户点击取消或关闭这个弹窗,会出现以下问题:
- **现象**: UI 上的 Switch 开关没有正确回到关闭状态,仍然显示为打开
- **实际情况**: 节点已经尝试连接(但因权限被拒绝而失败)
- **用户体验**: UI 状态与实际连接状态不一致,造成困惑
## 问题根源分析
### 1. Android 原生层面
`android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt` 文件中:
```kotlin
override fun openTun(options: TunOptions): Int {
if (prepare(this) != null) error("android: missing vpn permission") // 第76行
// ...
}
```
当用户拒绝 VPN 权限时,`prepare(this)` 返回非空值,代码会抛出 "android: missing vpn permission" 错误。
### 2. Flutter 服务层面
`lib/app/services/singbox_imp/kr_sing_box_imp.dart` 文件中的 `kr_start()` 方法:
**修复前的问题**:
- 当启动失败时,虽然设置了 `kr_status.value = SingboxStopped()`
- 但没有强制触发状态观察者的更新
- 导致 UI 层的 `Obx` 观察者可能无法及时响应状态变化
### 3. Controller 层面
`lib/app/modules/kr_home/controllers/kr_home_controller.dart``kr_toggleSwitch()` 方法中:
**修复前的问题**:
- 异常捕获后只记录了日志
- 没有主动触发 UI 状态同步
- 导致 Switch 开关状态与实际连接状态不一致
## 修复方案
### 修改 1: 增强 SingBox 服务层的状态刷新
**文件**: `lib/app/services/singbox_imp/kr_sing_box_imp.dart:427-465`
**修改内容**:
```dart
Future<void> kr_start() async {
kr_status.value = SingboxStarting();
try {
// ... 启动逻辑 ...
await kr_singBox.start(_cutPath, kr_configName, false).map(
(r) {
KRLogUtil.kr_i('✅ SingBox 启动成功', tag: 'SingBox');
},
).mapLeft((err) {
KRLogUtil.kr_e('❌ SingBox 启动失败: $err', tag: 'SingBox');
// 确保状态重置为Stopped触发UI更新
kr_status.value = SingboxStopped();
// 强制刷新状态以触发观察者 ← 新增
kr_status.refresh();
throw err;
}).run();
} catch (e, stackTrace) {
KRLogUtil.kr_e('💥 SingBox 启动异常: $e', tag: 'SingBox');
KRLogUtil.kr_e('📚 错误堆栈: $stackTrace', tag: 'SingBox');
// 确保状态重置为Stopped触发UI更新
kr_status.value = SingboxStopped();
// 强制刷新状态以触发观察者 ← 新增
kr_status.refresh();
rethrow;
}
}
```
**关键改动**:
- 在两处异常处理中都添加了 `kr_status.refresh()` 调用
- 确保即使状态值相同(都是 Stopped也能强制触发观察者更新
### 修改 2: 增强 Controller 的异常处理
**文件**: `lib/app/modules/kr_home/controllers/kr_home_controller.dart:529-558`
**修改内容**:
```dart
void kr_toggleSwitch(bool value) async {
// 如果正在切换中,直接返回
if (kr_isSwitching) {
KRLogUtil.kr_i('正在切换中,忽略本次操作', tag: 'HomeController');
return;
}
try {
kr_isSwitching = true;
if (value) {
await KRSingBoxImp.instance.kr_start();
// 添加延迟验证,确保状态正确更新
Future.delayed(const Duration(seconds: 2), () {
kr_forceSyncConnectionStatus();
});
} else {
await KRSingBoxImp.instance.kr_stop();
}
} catch (e) {
KRLogUtil.kr_e('切换失败: $e', tag: 'HomeController');
// 当启动失败时如VPN权限被拒绝强制同步状态 ← 新增
Future.delayed(const Duration(milliseconds: 100), () {
kr_forceSyncConnectionStatus();
});
} finally {
// 确保在任何情况下都会重置标志
kr_isSwitching = false;
}
}
```
**关键改动**:
-`catch` 块中添加了状态强制同步逻辑
- 延迟 100ms 调用 `kr_forceSyncConnectionStatus()` 确保状态同步到 UI
- 这样当 VPN 权限被拒绝导致启动失败时UI 会正确回退到断开状态
## 技术细节
### 状态流转过程
1. **用户点击 Switch 开启**
- UI 调用 `kr_toggleSwitch(true)`
- Controller 调用 `KRSingBoxImp.instance.kr_start()`
- SingBox 服务设置状态为 `SingboxStarting()`
2. **VPN 权限被拒绝场景**
- Android 原生层 `VPNService.openTun()` 抛出权限错误
- 错误传播到 SingBox 服务层
- `kr_start()` 的异常处理捕获错误
- 设置状态为 `SingboxStopped()` 并调用 `refresh()`
- Controller 的异常处理触发 `kr_forceSyncConnectionStatus()`
3. **UI 状态同步**
- `kr_status.refresh()` 强制触发 GetX 的 `Obx` 观察者
- `kr_forceSyncConnectionStatus()` 确保所有 UI 状态变量同步
- Switch 开关状态通过 `kr_isConnected.value` 回退到 `false`
### 为什么需要 refresh() ?
GetX 的响应式系统通过值比较来决定是否通知观察者:
- 如果 `kr_status.value``Starting` 变为 `Stopped`,会自动通知
- 但某些边界情况下,如果状态转换没有被正确捕获,调用 `refresh()` 可以强制通知所有观察者,不管值是否改变
### 为什么需要延迟调用?
- `Future.delayed(const Duration(milliseconds: 100))` 确保异步操作链完成
- 避免在状态还在转换过程中就尝试同步,导致读取到中间状态
## 测试验证
### 测试场景 1: 首次启动拒绝权限
1. 安装 App 后首次启动
2. 点击连接按钮
3. 系统弹出 VPN 权限请求
4. 点击"取消"或直接关闭弹窗
5. **预期结果**: Switch 自动回到关闭状态
### 测试场景 2: 二次启动(已授权)
1. 用户之前已授权 VPN 权限
2. 点击连接按钮
3. **预期结果**: 正常连接Switch 保持开启状态
### 测试场景 3: 撤销权限后重连
1. 在系统设置中撤销 VPN 权限
2. 返回 App 点击连接
3. 系统再次弹出权限请求
4. 拒绝权限
5. **预期结果**: Switch 自动回到关闭状态
## 影响范围
- **影响平台**: 仅 Android 平台
- **影响功能**: VPN 连接开关
- **向后兼容**: 完全兼容,不影响已有功能
- **性能影响**: 无明显性能影响,仅在异常情况下多执行一次状态同步
## 相关文件
1. `lib/app/services/singbox_imp/kr_sing_box_imp.dart` - SingBox 服务层
2. `lib/app/modules/kr_home/controllers/kr_home_controller.dart` - 主页控制器
3. `lib/app/modules/kr_home/views/kr_home_connection_info_view.dart` - Switch UI 组件
4. `android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt` - Android VPN 服务
## 修复日期
2025-10-03
## 修复作者
collins
OmnTech 提供技术支持