windows_core_api.dart 12 KB

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