windows_core_api.dart 12 KB

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