diff --git a/README.md b/README.md index c48c7b3..7e5fd19 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,6 @@ 国内下载地址: https://gitee.com/wanghongenpin/network-proxy-flutter/releases -iOS美版香港AppStore下载地址或直接搜ProxyPin: https://apps.apple.com/app/proxypin/id6450932949 - iOS国内TF下载地址(有1万名额限制,满了会清理不使用的用户): https://testflight.apple.com/join/gURGH6B4 TG: https://t.me/proxypin_tg diff --git a/README_EN.md b/README_EN.md index 9e26887..937f78b 100644 --- a/README_EN.md +++ b/README_EN.md @@ -15,9 +15,7 @@ and easy to use. **Mac will prompt untrusted developers when first opened, you need to go to System Preferences-Security & Privacy-Allow any source.** Download: https://github.com/wanghongenpin/network_proxy_flutter/releases -iOS AppStore ProxyPin: https://apps.apple.com/app/proxypin/id6450932949 - -iOS TestFlight(Limited by quota): https://testflight.apple.com/join/gURGH6B4 +iOS TestFlight Download APP(Limited by quota): https://testflight.apple.com/join/gURGH6B4 TG: https://t.me/proxypin_tg diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 789b773..df1650f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 55; objects = { /* Begin PBXBuildFile section */ diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 8911b27..972af6c 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -4,6 +4,9 @@ import NetworkExtension @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { + + var backgroundAudioEnable: Bool = false + override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -18,6 +21,7 @@ import NetworkExtension VpnManager.shared.disconnect() } else { let arguments = call.arguments as? Dictionary + self.backgroundAudioEnable = (arguments!["backgroundAudioEnable"]) as! Bool VpnManager.shared.connect(host: arguments?["proxyHost"] as? String ,port: arguments?["proxyPort"] as? Int) } }) @@ -54,37 +58,41 @@ import NetworkExtension bgTask = application.beginBackgroundTask(expirationHandler: nil) } - if (application.backgroundTimeRemaining <= 0 || application.applicationState == .active) { + if (application.backgroundTimeRemaining <= 0 || application.applicationState == .active || AudioManager.shared.openBackgroundAudioAutoplay) { timer?.invalidate(); timer = nil; } - if (!AudioManager.shared.openBackgroundAudioAutoplay) { - AudioManager.shared.openBackgroundAudioAutoplay = true - self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(expirationHandler: { - self.endBackgroundUpdateTask() - }) + if (application.backgroundTimeRemaining <= 10) { + self.backgroundAudio() } - + } override func applicationWillResignActive(_ application: UIApplication) { - if (!VpnManager.shared.isRunning()) { + self.backgroundAudio(); + } + override func applicationDidBecomeActive(_ application: UIApplication) { + self.endBackgroundUpdateTask() + } + + private func backgroundAudio() { + if (!VpnManager.shared.isRunning() || !self.backgroundAudioEnable) { return } + if (AudioManager.shared.openBackgroundAudioAutoplay) { + return; + } AudioManager.shared.openBackgroundAudioAutoplay = true self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(expirationHandler: { self.endBackgroundUpdateTask() }) } - override func applicationDidBecomeActive(_ application: UIApplication) { - self.endBackgroundUpdateTask() - } var backgroundUpdateTask: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier(rawValue: 0) func endBackgroundUpdateTask() { - if (!VpnManager.shared.isRunning()) { + if (!VpnManager.shared.isRunning() || !AudioManager.shared.openBackgroundAudioAutoplay) { return } diff --git a/ios/Runner/AudioManager.swift b/ios/Runner/AudioManager.swift index 042506d..8bb52cf 100644 --- a/ios/Runner/AudioManager.swift +++ b/ios/Runner/AudioManager.swift @@ -50,7 +50,7 @@ class AudioManager: NSObject { debugPrint("\(type(of:self)):\(error)") } self.backgroundAudioPlayer?.numberOfLoops = -1 - self.backgroundAudioPlayer?.volume = 1 + self.backgroundAudioPlayer?.volume = 0 self.backgroundAudioPlayer?.delegate = self } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f49480c..39560ae 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,11 +2,6 @@ - CFBundleLocalizations - - en - zh - BGTaskSchedulerPermittedIdentifiers $(PRODUCT_BUNDLE_IDENTIFIER) @@ -23,6 +18,11 @@ $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 + CFBundleLocalizations + + en + zh + CFBundleName ProxyPin CFBundlePackageType @@ -40,12 +40,15 @@ NSAllowsArbitraryLoads + NSCameraUsageDescription + 扫描二维码 + NSPhotoLibraryUsageDescription + 请求重写选择文件 UIApplicationSupportsIndirectInputEvents UIBackgroundModes audio - processing UILaunchStoryboardName LaunchScreen @@ -66,9 +69,5 @@ UIViewControllerBasedStatusBarAppearance - NSCameraUsageDescription - 扫描二维码 - NSPhotoLibraryUsageDescription - 请求重写选择文件 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 957204d..dc05bc9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -222,6 +222,8 @@ "enableScript": "Enable Script", "scriptUseDescribe": "Use JavaScript to modify requests and responses", "scriptEdit": "Edit script", - "material3": "Material 3 is the latest version of Google’s open-source design system" + "material3": "Material 3 is the latest version of Google’s open-source design system", + + "iosVpnBackgroundAudio": "After turning on packet capture, exit to the background. In order to maintain the main UI thread for network communication, a silent audio playback will be enabled to keep the main thread running. Otherwise, it will only run in the background for 30 seconds. Do you agree to play audio in the background after turning on packet capture?" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 9b0e80c..7297e74 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -222,5 +222,6 @@ "enableScript": "启用脚本工具", "scriptUseDescribe": "使用 JavaScript 修改请求和响应", "scriptEdit": "编辑脚本", - "material3": "Material3是谷歌开源设计系统的最新版本" + "material3": "Material3是谷歌开源设计系统的最新版本", + "iosVpnBackgroundAudio": "开启抓包后,退出到后台。为了维护主UI线程的网络通信,将启用静音音频播放以保持主线程运行。否则,它将只在后台运行30秒。您同意在启用抓包后在后台播放音频吗?" } \ No newline at end of file diff --git a/lib/native/vpn.dart b/lib/native/vpn.dart index e0cc15a..f86a5ac 100644 --- a/lib/native/vpn.dart +++ b/lib/native/vpn.dart @@ -5,8 +5,9 @@ class Vpn { static bool isVpnStarted = false; //vpn是否已经启动 - static startVpn(String host, int port, [List? appList]) { - proxyVpnChannel.invokeMethod("startVpn", {"proxyHost": host, "proxyPort": port, "allowApps": appList}); + static startVpn(String host, int port, {List? appList, bool? backgroundAudioEnable = true}) { + proxyVpnChannel.invokeMethod("startVpn", + {"proxyHost": host, "proxyPort": port, "allowApps": appList, "backgroundAudioEnable": backgroundAudioEnable}); isVpnStarted = true; } @@ -16,8 +17,10 @@ class Vpn { } //重启vpn - static restartVpn(String host, int port, [List? appList]) { - proxyVpnChannel.invokeMethod("restartVpn", {"proxyHost": host, "proxyPort": port, "allowApps": appList}); + static restartVpn(String host, int port, {List? appList, bool? backgroundAudioEnable = true}) { + proxyVpnChannel.invokeMethod("restartVpn", + {"proxyHost": host, "proxyPort": port, "allowApps": appList, "backgroundAudioEnable": backgroundAudioEnable}); + isVpnStarted = true; } diff --git a/lib/network/http/http_headers.dart b/lib/network/http/http_headers.dart index 40218a5..159bd2e 100644 --- a/lib/network/http/http_headers.dart +++ b/lib/network/http/http_headers.dart @@ -19,9 +19,9 @@ import 'dart:collection'; class HttpHeaders { - static const CONTENT_LENGTH = "Content-Length"; - static const CONTENT_ENCODING = "Content-Encoding"; - static const CONTENT_TYPE = "Content-Type"; + static const String CONTENT_LENGTH = "Content-Length"; + static const String CONTENT_ENCODING = "Content-Encoding"; + static const String CONTENT_TYPE = "Content-Type"; static const String HOST = "Host"; static const String TRANSFER_ENCODING = "Transfer-Encoding"; static const String Cookie = "Cookie"; @@ -110,11 +110,11 @@ class HttpHeaders { set contentLength(int contentLength) => set(CONTENT_LENGTH, contentLength.toString()); - String? get contentEncoding => get(HttpHeaders.CONTENT_ENCODING); + String? get contentEncoding => get(HttpHeaders.CONTENT_ENCODING)?.toLowerCase(); bool get isGzip => contentEncoding == "gzip"; - bool get isChunked => get(HttpHeaders.TRANSFER_ENCODING) == "chunked"; + bool get isChunked => get(HttpHeaders.TRANSFER_ENCODING)?.toLowerCase() == "chunked"; String get cookie => get(Cookie) ?? ""; diff --git a/lib/storage/histories.dart b/lib/storage/histories.dart index 09395ac..47899f3 100644 --- a/lib/storage/histories.dart +++ b/lib/storage/histories.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:date_format/date_format.dart'; import 'package:file_selector/file_selector.dart'; import 'package:network_proxy/network/http/http.dart'; +import 'package:network_proxy/network/util/logger.dart'; import 'package:network_proxy/utils/files.dart'; import 'package:network_proxy/utils/har.dart'; import 'package:path_provider/path_provider.dart'; @@ -122,6 +123,23 @@ class HistoryStorage { return history.requests!; } + ///刷新requests + Future flushRequests(HistoryItem history, List requests) async { + logger.i("刷新历史记录 $history"); + final homePath = await _homePath(); + String path = '$homePath${Platform.pathSeparator}${Files.getName(history.path)}'; + var file = File(path); + for (int i = 0; i < requests.length; i++) { + var request = requests[i]; + var har = Har.toHar(request); + await file.writeAsString("${jsonEncode(har)},\n", mode: i == 0 ? FileMode.write : FileMode.append); + } + + history.requestLength = requests.length; + await file.length().then((size) => history.fileSize = size); + (await _path).writeAsString(jsonEncode(_histories)); + } + //添加历史 Future addHarFile(XFile file) async { var readAsBytes = await file.readAsString(); diff --git a/lib/ui/component/utils.dart b/lib/ui/component/utils.dart index f665cb4..07b2520 100644 --- a/lib/ui/component/utils.dart +++ b/lib/ui/component/utils.dart @@ -163,10 +163,10 @@ Future showContextMenu(BuildContext context, Offset offset, {required List showConfirmDialog(BuildContext context, {String? title, String? content, VoidCallback? onConfirm}) { title ??= AppLocalizations.of(context)!.confirmTitle; content ??= AppLocalizations.of(context)!.confirmContent; - showDialog( + return showDialog( context: context, builder: (context) { return AlertDialog( diff --git a/lib/ui/configuration.dart b/lib/ui/configuration.dart index 1ef5e24..8e48971 100644 --- a/lib/ui/configuration.dart +++ b/lib/ui/configuration.dart @@ -34,6 +34,8 @@ class AppConfiguration { /// bool headerExpanded = true; + bool? iosVpnBackgroundAudioEnable; + AppConfiguration._(); /// 单例 @@ -116,6 +118,7 @@ class AppConfiguration { _language = config['language'] == null ? null : Locale.fromSubtags(languageCode: config['language']); smallWindow = config['smallWindow'] ?? Platform.isAndroid; headerExpanded = config['headerExpanded'] ?? true; + iosVpnBackgroundAudioEnable = config['iosVpnBackgroundAudioEnable']; } catch (e) { print(e); } @@ -140,7 +143,8 @@ class AppConfiguration { 'upgradeNoticeV7': upgradeNoticeV7, "language": _language?.languageCode, 'smallWindow': smallWindow, - "headerExpanded": headerExpanded + "headerExpanded": headerExpanded, + "iosVpnBackgroundAudioEnable": iosVpnBackgroundAudioEnable == false ? null : iosVpnBackgroundAudioEnable }; } } diff --git a/lib/ui/desktop/left/history.dart b/lib/ui/desktop/left/history.dart index 068b82f..019a2d5 100644 --- a/lib/ui/desktop/left/history.dart +++ b/lib/ui/desktop/left/history.dart @@ -43,7 +43,7 @@ class HistoryPageWidget extends StatelessWidget { return MaterialPageRoute( builder: (_) => futureWidget( HistoryStorage.instance, - (storage) => _HistoryWidget(storage, + (storage) => _HistoryListWidget(storage, container: domainWidgetState.currentState!.container, proxyServer: proxyServer), )); } @@ -72,21 +72,20 @@ class HistoryPageWidget extends StatelessWidget { } } -class _HistoryWidget extends StatefulWidget { +///历史记录列表 +class _HistoryListWidget extends StatefulWidget { // 存储 final HistoryStorage storage; final List container; final ProxyServer proxyServer; - const _HistoryWidget(this.storage, {required this.container, required this.proxyServer}); + const _HistoryListWidget(this.storage, {required this.container, required this.proxyServer}); @override - State createState() { - return _HistoryState(); - } + State createState() => _HistoryListState(); } -class _HistoryState extends State<_HistoryWidget> { +class _HistoryListState extends State<_HistoryListWidget> { ///是否保存会话 static bool _sessionSaved = false; static WriteTask? writeTask; @@ -146,7 +145,7 @@ class _HistoryState extends State<_HistoryWidget> { try { var historyItem = await storage.addHarFile(file); setState(() { - Navigator.pushNamed(context, '/domain', arguments: {'item': historyItem}); + toRequestsView(historyItem); FlutterToastr.show(localizations.importSuccess, context); }); } catch (e, t) { @@ -212,10 +211,17 @@ class _HistoryState extends State<_HistoryWidget> { dense: true, title: Text(item.name), subtitle: Text(localizations.historySubtitle(item.requestLength, item.size)), - onTap: () { - Navigator.pushNamed(context, '/domain', arguments: {'item': item}) - .whenComplete(() => Future.delayed(const Duration(seconds: 60), () => item.requests = null)); - })); + onTap: () => toRequestsView(item))); + } + + toRequestsView(HistoryItem item) { + Navigator.pushNamed(context, '/domain', arguments: {'item': item}).whenComplete(() async { + if (item != writeTask?.history &&item.requests != null && item.requestLength != item.requests?.length) { + await widget.storage.flushRequests(item, item.requests!); + setState(() {}); + } + Future.delayed(const Duration(seconds: 60), () => item.requests = null); + }); } //重命名 @@ -316,9 +322,7 @@ class WriteTask extends EventListener { if (writeList.isEmpty) { return; } - int length = history.requestLength; - while (writeList.isNotEmpty) { var request = writeList.removeFirst(); var har = Har.toHar(request); @@ -332,6 +336,7 @@ class WriteTask extends EventListener { history.requestLength = length; history.fileSize = await open.length(); + history.requests = null; await historyStorage.refresh(); callback?.call(); } diff --git a/lib/ui/desktop/left/list.dart b/lib/ui/desktop/left/list.dart index b8846a5..2ee09b6 100644 --- a/lib/ui/desktop/left/list.dart +++ b/lib/ui/desktop/left/list.dart @@ -60,11 +60,16 @@ class DomainWidgetState extends State with AutomaticKeepAliveClientM var host = request.remoteDomain()!; DomainRequests? domainRequests = containerMap[host]; if (domainRequests == null) { - domainRequests = DomainRequests(host, proxyServer: widget.panel.proxyServer, onRemove: () => remove(host)); + domainRequests = DomainRequests(host, proxyServer: widget.panel.proxyServer, onRemove: () { + widget.list?.removeWhere((it) => it.remoteDomain() == host); + remove(host); + }); containerMap[host] = domainRequests; } - var listURI = RequestWidget(request, widget.panel, - proxyServer: widget.panel.proxyServer, remove: (it) => domainRequests!.remove(it)); + var listURI = RequestWidget(request, widget.panel, proxyServer: widget.panel.proxyServer, remove: (it) { + widget.list?.remove(it.request); + domainRequests!.remove(it); + }); domainRequests.addBody(null, listURI); }); } @@ -87,7 +92,7 @@ class DomainWidgetState extends State with AutomaticKeepAliveClientM Widget body = widget.shrinkWrap ? SingleChildScrollView(child: Column(children: list.toList())) - : ListView.builder(itemCount: list.length, cacheExtent: 1000, itemBuilder: (_, index) => list.elementAt(index)); + : ListView.builder(itemCount: list.length, itemBuilder: (_, index) => list.elementAt(index)); return Scaffold( body: body, diff --git a/lib/ui/desktop/toolbar/setting/filter.dart b/lib/ui/desktop/toolbar/setting/filter.dart index 4b44978..20be031 100644 --- a/lib/ui/desktop/toolbar/setting/filter.dart +++ b/lib/ui/desktop/toolbar/setting/filter.dart @@ -46,7 +46,7 @@ class _FilterDialogState extends State { ]), content: SizedBox( width: 680, - height: 520, + height: 510, child: Flex( direction: Axis.horizontal, children: [ diff --git a/lib/ui/launch/launch.dart b/lib/ui/launch/launch.dart index a82935c..2225f7e 100644 --- a/lib/ui/launch/launch.dart +++ b/lib/ui/launch/launch.dart @@ -118,10 +118,10 @@ class _SocketLaunchState extends State with WindowListener, Widget } ///启动代理服务器 - start() { + start() async { if (!widget.serverLaunch) { + await widget.onStart?.call(); setState(() { - widget.onStart?.call(); SocketLaunch.started = true; }); return; diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index 4883e5d..6f7349d 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -192,8 +192,19 @@ class MobileHomeState extends State implements EventListener, Li size: 36, startup: false, serverLaunch: false, - onStart: () => Vpn.startVpn( - Platform.isAndroid ? data : "127.0.0.1", proxyServer.port, proxyServer.configuration.appWhitelist), + onStart: () async { + // if (Platform.isIOS && widget.appConfiguration.iosVpnBackgroundAudioEnable == null) { + // widget.appConfiguration.iosVpnBackgroundAudioEnable = false; + // await showConfirmDialog(context, content: localizations.iosVpnBackgroundAudio, onConfirm: () { + // widget.appConfiguration.iosVpnBackgroundAudioEnable = true; + // widget.appConfiguration.flushConfig(); + // }); + // } + + Vpn.startVpn(Platform.isAndroid ? data : "127.0.0.1", proxyServer.port, + backgroundAudioEnable: widget.appConfiguration.iosVpnBackgroundAudioEnable, + appList: proxyServer.configuration.appWhitelist); + }, onStop: () => Vpn.stopVpn()); })), ); diff --git a/lib/ui/mobile/request/history.dart b/lib/ui/mobile/request/history.dart index 08098c5..3528009 100644 --- a/lib/ui/mobile/request/history.dart +++ b/lib/ui/mobile/request/history.dart @@ -1,3 +1,18 @@ +/* + * Copyright 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import 'dart:async'; import 'dart:collection'; import 'dart:convert'; @@ -106,7 +121,7 @@ class _MobileHistoryState extends State { try { var historyItem = await storage.addHarFile(file); setState(() { - Navigator.pushNamed(context, '/domain', arguments: {'item': historyItem}); + toRequestsView(historyItem, storage); FlutterToastr.show(localizations.importSuccess, context); }); } catch (e, t) { @@ -150,22 +165,30 @@ class _MobileHistoryState extends State { selected: selectIndex == index, title: Text(item.name), subtitle: Text(localizations.historySubtitle(item.requestLength, item.size)), - onTap: () { - Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) { - bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh'); - return Scaffold( - appBar: AppBar( - title: Text('${item.name} ${isCN ? "记录数" : "Records"} ${item.requestLength}', - style: const TextStyle(fontSize: 16))), - body: futureWidget( - loading: true, - storage.getRequests(item), - (data) => RequestListWidget(proxyServer: widget.proxyServer, list: data))); - })).then((value) => Future.delayed(const Duration(seconds: 60), () => item.requests = null)); - }, + onTap: () => toRequestsView(item, storage), )); } + toRequestsView(HistoryItem item, HistoryStorage storage) { + Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) { + bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh'); + return Scaffold( + appBar: AppBar( + title: Text('${item.name} ${isCN ? "记录数" : "Records"} ${item.requestLength}', + style: const TextStyle(fontSize: 16))), + body: futureWidget( + loading: true, + storage.getRequests(item), + (data) => RequestListWidget(proxyServer: widget.proxyServer, list: data))); + })).then((value) async { + if (item != writeTask?.history && item.requests != null && item.requestLength != item.requests?.length) { + await storage.flushRequests(item, item.requests!); + setState(() {}); + } + Future.delayed(const Duration(seconds: 60), () => item.requests = null); + }); + } + //导出har export(HistoryStorage storage, HistoryItem item) async { //文件名称 @@ -217,8 +240,7 @@ class _MobileHistoryState extends State { context: context, builder: (ctx) { return AlertDialog( - title: - Text(localizations.historyDeleteConfirm, style: const TextStyle(fontSize: 18)), + title: Text(localizations.historyDeleteConfirm, style: const TextStyle(fontSize: 18)), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: Text(localizations.cancel)), TextButton( diff --git a/lib/ui/mobile/setting/app_whitelist.dart b/lib/ui/mobile/setting/app_whitelist.dart index ae95055..7e2e2ff 100644 --- a/lib/ui/mobile/setting/app_whitelist.dart +++ b/lib/ui/mobile/setting/app_whitelist.dart @@ -7,6 +7,7 @@ import 'package:installed_apps/installed_apps.dart'; import 'package:network_proxy/native/vpn.dart'; import 'package:network_proxy/network/bin/configuration.dart'; import 'package:network_proxy/network/bin/server.dart'; +import 'package:network_proxy/ui/configuration.dart'; //应用白名单 目前只支持安卓 ios没办法获取安装的列表 class AppWhitelist extends StatefulWidget { @@ -34,7 +35,9 @@ class _AppWhitelistState extends State { @override void dispose() { if (changed && widget.proxyServer.isRunning) { - Vpn.restartVpn("127.0.0.1", widget.proxyServer.port, configuration.appWhitelist); + Vpn.restartVpn("127.0.0.1", widget.proxyServer.port, + appList: configuration.appWhitelist, + backgroundAudioEnable: AppConfiguration.current?.iosVpnBackgroundAudioEnable); configuration.flushConfig(); } super.dispose();