windows_core_api.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:io';
  4. import 'package:flutter/services.dart';
  5. import 'package:get/get.dart';
  6. import 'package:nomo/app/controllers/windows/window_service.dart';
  7. import 'package:path/path.dart' as path;
  8. import 'package:path_provider/path_provider.dart';
  9. import 'package:uuid/uuid.dart';
  10. import '../../config/translations/strings_enum.dart';
  11. import '../../utils/log/logger.dart';
  12. import '../constants/configs.dart';
  13. import '../constants/enums.dart';
  14. import '../constants/errors.dart';
  15. import 'base_core_api.dart';
  16. import 'windows/menu_base/src/menu.dart';
  17. import 'windows/menu_base/src/menu_item.dart';
  18. import 'windows/stat_log_entry.dart';
  19. import 'windows/vpn_service.dart';
  20. /// Windows 实现
  21. class WindowsCoreApi implements BaseCoreApi {
  22. static const _tag = 'WindowsCoreApi';
  23. WindowsCoreApi._() {
  24. // 初始化事件通道
  25. _initEventChannel();
  26. // 初始化vpn服务
  27. _initVpnService();
  28. // 初始化窗口服务
  29. _initWindowService();
  30. }
  31. // 创建vpn服务
  32. static final _vpn = VpnService();
  33. // 创建窗口服务
  34. static final _windowService = WindowService();
  35. // 检查定时器
  36. Timer? _checkTimer;
  37. bool _hasConnectedOnce = false;
  38. bool _isVpnInited = false;
  39. int _remainTime = 0x7FFFFFFFFFFFFFFF; // 会员剩余时间
  40. int _sessionCountUp = 0; // session计时
  41. bool _isCountdown = false; // 汇报计时类型 倒计时还是正计时
  42. int _locationId = 0;
  43. String _locationCode = '';
  44. String _boostSessionId = '';
  45. /// 内部构造方法,供 BaseCoreApi 工厂使用
  46. factory WindowsCoreApi.create() => WindowsCoreApi._();
  47. // Windows Method Channel
  48. static const MethodChannel _channel = MethodChannel('app.xixi.nomo/core_api');
  49. // Windows 事件流控制器
  50. static final StreamController<String> _eventController =
  51. StreamController<String>.broadcast();
  52. // Windows 事件流
  53. static Stream<String> get eventStream => _eventController.stream;
  54. // 初始化事件监听
  55. void _initEventChannel() {
  56. // 监听来自 Windows 原生端的方法调用
  57. _channel.setMethodCallHandler(_handleMethodCall);
  58. }
  59. // 处理来自 Windows 原生端的方法调用
  60. Future<dynamic> _handleMethodCall(MethodCall call) async {
  61. switch (call.method) {
  62. case 'onEventChange':
  63. // 原生端发送事件,转发到事件流
  64. final String event = call.arguments as String;
  65. _eventController.add(event);
  66. return null;
  67. default:
  68. throw PlatformException(
  69. code: 'Unimplemented',
  70. message: 'Method ${call.method} not implemented',
  71. );
  72. }
  73. }
  74. void _initWindowService() {
  75. _windowService.initialize();
  76. _updatTrayIcon();
  77. _setTrayMenu();
  78. }
  79. void _initVpnService() {
  80. if (_isVpnInited) {
  81. return;
  82. }
  83. _isVpnInited = true;
  84. // 初始化vpn服务 10秒超时
  85. _vpn.initialize(10, false);
  86. // 监听VPN服务状态
  87. _vpn.onStatusChanged.listen((event) async {
  88. final (status, data) = event;
  89. // 处理VPN连接状态
  90. switch (status) {
  91. case VpnStatus.connecting:
  92. _handleStateConnecting();
  93. break;
  94. case VpnStatus.connected:
  95. _handleStateConnected();
  96. break;
  97. case VpnStatus.error:
  98. final code = data != null ? data as int : -1;
  99. _handleStateError(code);
  100. break;
  101. case VpnStatus.idle:
  102. _handleStateDisconnected();
  103. break;
  104. }
  105. // 更新托盘图标
  106. _updatTrayIcon();
  107. });
  108. }
  109. void _handleStateConnecting() {
  110. _hasConnectedOnce = false;
  111. _sessionCountUp = 0;
  112. // 正在连接
  113. _eventController.add(
  114. '{"type":"vpn_status","status":1,"code":0,"message":""}',
  115. );
  116. }
  117. void _handleStateConnected() {
  118. // 只记录第一次连接成功的时间戳
  119. if (!_hasConnectedOnce) {
  120. _sessionCountUp = 0;
  121. }
  122. _hasConnectedOnce = true;
  123. // 创建检测定时器
  124. _checkTimer ??= Timer.periodic(const Duration(seconds: 1), (_) {
  125. // 累加1秒
  126. _sessionCountUp += 1000;
  127. // 累减1秒
  128. _remainTime -= 1000;
  129. // 检查用户会员剩余时间
  130. _checkMembershipRemaining();
  131. // 更新连接时长
  132. _updateSessionDuration();
  133. });
  134. // 通知 已经连接
  135. _eventController.add(
  136. '{"type":"vpn_status","status":2,"code":0,"message":""}',
  137. );
  138. // 获取统计
  139. _vpn.queryStat().then((stat) {
  140. log(_tag, 'stat: $stat');
  141. try {
  142. final ts = DateTime.now().millisecondsSinceEpoch;
  143. final connectionHistory = List<ConnectionHistory>.from(
  144. (stat?['connectionHistory'] ?? []).map(
  145. (x) => ConnectionHistory.fromJson(x),
  146. ),
  147. );
  148. final param = jsonEncode([
  149. StatLogEntry(
  150. id: Uuid().v4(),
  151. time: ts,
  152. level: 'info',
  153. module: 'NM_BoostResult',
  154. category: 'nomo',
  155. fields: Fields(
  156. code: 0,
  157. boostSessionId: _boostSessionId,
  158. success: true,
  159. locationId: _locationId,
  160. locationCode: _locationCode,
  161. generatedTime: ts,
  162. connectionHistory: connectionHistory,
  163. ),
  164. ),
  165. ]);
  166. final nodeId = connectionHistory.last.nodeId ?? '';
  167. final msg = jsonEncode({
  168. "type": "boost_result",
  169. "locationCode": _locationCode,
  170. "nodeId": nodeId,
  171. "success": true,
  172. "param": param,
  173. });
  174. log(_tag, msg);
  175. _eventController.add(msg);
  176. } catch (e) {
  177. log(_tag, 'parse stat json error: $e');
  178. }
  179. });
  180. }
  181. void _handleStateError(int code) {
  182. _eventController.add(
  183. '{"type":"vpn_status","status":3,"code":$code,"message":""}',
  184. );
  185. // 获取统计
  186. _vpn.queryStat().then((stat) {
  187. log(_tag, 'stat: $stat');
  188. try {
  189. final ts = DateTime.now().millisecondsSinceEpoch;
  190. final connectionHistory = List<ConnectionHistory>.from(
  191. (stat?['connectionHistory'] ?? []).map(
  192. (x) => ConnectionHistory.fromJson(x),
  193. ),
  194. );
  195. final param = jsonEncode([
  196. StatLogEntry(
  197. id: Uuid().v4(),
  198. time: ts,
  199. level: 'info',
  200. module: 'NM_BoostResult',
  201. category: 'nomo',
  202. fields: Fields(
  203. code: code,
  204. boostSessionId: _boostSessionId,
  205. success: false,
  206. locationId: _locationId,
  207. locationCode: _locationCode,
  208. generatedTime: ts,
  209. connectionHistory: connectionHistory,
  210. ),
  211. ),
  212. ]);
  213. final nodeId = connectionHistory.last.nodeId ?? '';
  214. final msg =
  215. '{"type":"boost_result","locationCode":"$_locationCode","nodeId":"$nodeId","success":false, "param": $param}';
  216. _eventController.add(msg);
  217. } catch (e) {
  218. log(_tag, 'parse stat json error: $e');
  219. }
  220. });
  221. _vpn.stop();
  222. }
  223. void _handleStateDisconnected() {
  224. _checkTimer?.cancel();
  225. _checkTimer = null;
  226. final isNoRemainTime = _remainTime <= 0;
  227. if (isNoRemainTime) {
  228. _eventController.add(
  229. '{"type":"vpn_status","status":3,"code":${Errors.ERROR_REMAIN_TIME},"message":""}',
  230. );
  231. } else {
  232. _eventController.add(
  233. '{"type":"vpn_status","status":0,"code":0,"message":""}',
  234. );
  235. }
  236. }
  237. void _checkMembershipRemaining() {
  238. // 没有会员时间
  239. if (_remainTime < 1000) {
  240. log(_tag, 'no remain time, need to disconnect.');
  241. // 断开vpn
  242. _vpn.stop();
  243. }
  244. }
  245. void _updateSessionDuration() {
  246. if (_isCountdown) {
  247. _eventController.add(
  248. '{"type":"timer_update","currentTime":$_remainTime,"mode":1}',
  249. );
  250. } else {
  251. _eventController.add(
  252. '{"type":"timer_update","currentTime":$_sessionCountUp,"mode":0}',
  253. );
  254. }
  255. }
  256. void _updatTrayIcon() {
  257. final isDark = Get.theme.brightness == Brightness.dark;
  258. // 更新提示栏
  259. _windowService.setSystemTrayIcon(
  260. _vpn.status == VpnStatus.connected,
  261. isDark,
  262. Configs.appName,
  263. );
  264. }
  265. void _setTrayMenu() {
  266. final trayMenu = Menu(
  267. items: [
  268. MenuItem(label: Strings.showWindow.tr, key: 'active'),
  269. MenuItem.separator(),
  270. MenuItem(label: Strings.quitApp.tr, key: 'quit'),
  271. ],
  272. );
  273. _windowService.setSystemTrayMenu(trayMenu, (menuItem) {
  274. if (menuItem.key == 'quit') {
  275. _windowService.quitApplication();
  276. } else if (menuItem.key == 'active') {
  277. _windowService.activeWindow();
  278. }
  279. });
  280. }
  281. @override
  282. Future<String?> getApps() async {
  283. // Windows 不需要获取应用列表
  284. return null;
  285. }
  286. @override
  287. Future<String?> getSystemLocale() async {
  288. return Platform.localeName;
  289. }
  290. @override
  291. Future<bool?> connect(
  292. String sessionId,
  293. int socksPort,
  294. String tunnelConfig,
  295. String configJson,
  296. int remainTime,
  297. bool isCountdown,
  298. List<String> allowVpnApps,
  299. List<String> disallowVpnApps,
  300. String accessToken,
  301. String aesKey,
  302. String aesIv,
  303. int locationId,
  304. String locationCode,
  305. List<String> baseUrls,
  306. String params,
  307. int peekTimeInterval,
  308. ) async {
  309. // 记录会员剩余时间
  310. _remainTime = remainTime;
  311. _isCountdown = isCountdown;
  312. _locationId = locationId;
  313. _locationCode = locationCode;
  314. _boostSessionId = sessionId;
  315. String geoPath = await _getGeoDirectory();
  316. final selfExecutable = Platform.resolvedExecutable;
  317. List<String> allowExes = [];
  318. List<String> disallowExes = [selfExecutable];
  319. // 连接参数
  320. Map<String, dynamic> params = {
  321. 'sessionId': sessionId,
  322. 'connectOptions': jsonEncode({
  323. 'geoPath': geoPath,
  324. 'nodesConfig': configJson,
  325. }),
  326. 'allowExes': allowExes,
  327. 'disallowExes': disallowExes,
  328. };
  329. // 连接vpn
  330. _vpn.start(params);
  331. return true;
  332. }
  333. @override
  334. Future<bool?> disconnect() async {
  335. // 实现 Windows 断开连接逻辑
  336. await _vpn.stop();
  337. return true;
  338. }
  339. @override
  340. Future<String?> getRemoteIp() async {
  341. // 实现 Windows 获取远程 IP
  342. return await _vpn.getRemoteAddress();
  343. }
  344. @override
  345. Future<String?> getAdvertisingId() async {
  346. // Windows 不支持广告 ID
  347. return null;
  348. }
  349. @override
  350. Future<bool?> moveTaskToBack() async {
  351. // Windows 不需要此功能
  352. return true;
  353. }
  354. @override
  355. Future<bool?> isConnected() async {
  356. return _vpn.isOnline && _vpn.status == ConnectionState.connected;
  357. }
  358. @override
  359. Future<String?> getSimInfo() async {
  360. // Windows 不支持 SIM 卡信息
  361. return null;
  362. }
  363. @override
  364. Future<String?> getChannel() async {
  365. return 'windows';
  366. }
  367. @override
  368. Future<void> openPackage(String packageName) async {
  369. // Windows 不支持打开应用
  370. }
  371. /// 发送事件(供 Windows 实现内部使用)
  372. ///
  373. /// Windows 原生端可以通过 MethodChannel 发送事件:
  374. /// ```cpp
  375. /// // C++ 示例
  376. /// flutter::MethodChannel<flutter::EncodableValue> channel(
  377. /// messenger, "app.xixi.nomo/core_api",
  378. /// &flutter::StandardMethodCodec::GetInstance());
  379. ///
  380. /// // 发送 VPN 状态变化
  381. /// channel.InvokeMethod("onEventChange",
  382. /// flutter::EncodableValue("{\"type\":\"vpn_status\",\"status\":2}"));
  383. /// ```
  384. ///
  385. /// 事件 JSON 格式:
  386. /// - vpn_status: {"type":"vpn_status","status":0|1|2|3,"code":0,"message":""}
  387. /// - status: 0=idle, 1=connecting, 2=connected, 3=error
  388. /// - timer_update: {"type":"timer_update","currentTime":123,"mode":"countdown"}
  389. /// - boost_result: {"type":"boost_result","locationCode":"US","nodeId":"xxx","success":true}
  390. static void sendEvent(String event) {
  391. _eventController.add(event);
  392. }
  393. /// 释放资源
  394. static void dispose() {
  395. _vpn.dispose();
  396. _windowService.dispose();
  397. _eventController.close();
  398. }
  399. /// 获取 geo 文件目录
  400. Future<String> _getGeoDirectory() async {
  401. try {
  402. final appDir = await getApplicationSupportDirectory();
  403. final geoDir = Directory(path.join(appDir.path, 'geo'));
  404. return geoDir.path;
  405. } catch (_) {
  406. return '';
  407. }
  408. }
  409. }