# 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 提供技术支持
