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

6.6 KiB
Raw Blame History

Android VPN 权限弹窗关闭后 UI 状态不同步问题修复

问题描述

在 Android 平台上,当用户第一次打开 App 后点击连接按钮时,系统会弹出 VPN 权限请求弹窗。如果用户点击取消或关闭这个弹窗,会出现以下问题:

  • 现象: UI 上的 Switch 开关没有正确回到关闭状态,仍然显示为打开
  • 实际情况: 节点已经尝试连接(但因权限被拒绝而失败)
  • 用户体验: UI 状态与实际连接状态不一致,造成困惑

问题根源分析

1. Android 原生层面

android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt 文件中:

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.dartkr_toggleSwitch() 方法中:

修复前的问题:

  • 异常捕获后只记录了日志
  • 没有主动触发 UI 状态同步
  • 导致 Switch 开关状态与实际连接状态不一致

修复方案

修改 1: 增强 SingBox 服务层的状态刷新

文件: lib/app/services/singbox_imp/kr_sing_box_imp.dart:427-465

修改内容:

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

修改内容:

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.valueStarting 变为 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 提供技术支持