diff --git a/android/app/src/main/kotlin/com/network/proxy/ProxyVpnService.kt b/android/app/src/main/kotlin/com/network/proxy/ProxyVpnService.kt index b37f9342..81e232c3 100644 --- a/android/app/src/main/kotlin/com/network/proxy/ProxyVpnService.kt +++ b/android/app/src/main/kotlin/com/network/proxy/ProxyVpnService.kt @@ -31,6 +31,7 @@ class ProxyVpnService : VpnService(), ProtectSocket { const val PROXY_HOST_KEY = "ProxyHost" const val PROXY_PORT_KEY = "ProxyPort" const val ALLOW_APPS_KEY = "AllowApps" //允许的名单 + const val DISALLOW_APPS_KEY = "DisallowApps" //禁止的名单 /** * 动作:断开连接 @@ -48,6 +49,7 @@ class ProxyVpnService : VpnService(), ProtectSocket { var host: String? = null var port: Int = 0 var allowApps: ArrayList? = null + private var disallowApps: ArrayList? = null fun stopVpnIntent(context: Context): Intent { return Intent(context, ProxyVpnService::class.java).also { @@ -59,12 +61,14 @@ class ProxyVpnService : VpnService(), ProtectSocket { context: Context, proxyHost: String? = host, proxyPort: Int? = port, - allowApps: ArrayList? = this.allowApps + allowApps: ArrayList? = this.allowApps, + disallowApps: ArrayList? = this.disallowApps ): Intent { return Intent(context, ProxyVpnService::class.java).also { it.putExtra(PROXY_HOST_KEY, proxyHost) it.putExtra(PROXY_PORT_KEY, proxyPort) it.putStringArrayListExtra(ALLOW_APPS_KEY, allowApps) + it.putStringArrayListExtra(DISALLOW_APPS_KEY, disallowApps) } } } @@ -82,7 +86,8 @@ class ProxyVpnService : VpnService(), ProtectSocket { connect( intent.getStringExtra(PROXY_HOST_KEY) ?: host!!, intent.getIntExtra(PROXY_PORT_KEY, port), - intent.getStringArrayListExtra(ALLOW_APPS_KEY) ?: allowApps + intent.getStringArrayListExtra(ALLOW_APPS_KEY) ?: allowApps, + intent.getStringArrayListExtra(DISALLOW_APPS_KEY) ) START_STICKY } @@ -98,13 +103,19 @@ class ProxyVpnService : VpnService(), ProtectSocket { isRunning = false } - private fun connect(proxyHost: String, proxyPort: Int, allowPackages: ArrayList?) { + private fun connect( + proxyHost: String, + proxyPort: Int, + allowPackages: ArrayList?, + disallowPackages: ArrayList? + ) { Log.i("ProxyVpnService", "startVpn $host:$port $allowApps") host = proxyHost port = proxyPort allowApps = allowPackages - vpnInterface = createVpnInterface(proxyHost, proxyPort, allowPackages) + disallowApps = disallowPackages + vpnInterface = createVpnInterface(proxyHost, proxyPort, allowPackages, disallowPackages) if (vpnInterface == null) { val alertDialog = Intent(applicationContext, VpnAlertDialog::class.java) .setAction("com.network.proxy.ProxyVpnService") @@ -152,7 +163,12 @@ class ProxyVpnService : VpnService(), ProtectSocket { } - private fun createVpnInterface(proxyHost: String, proxyPort: Int, allowPackages: List?): + private fun createVpnInterface( + proxyHost: String, + proxyPort: Int, + allowPackages: List?, + disallowApps: ArrayList? + ): ParcelFileDescriptor? { val build = Builder() .setMtu(MAX_PACKET_LEN) @@ -170,6 +186,10 @@ class ProxyVpnService : VpnService(), ProtectSocket { build.addDisallowedApplication(baseContext.packageName) } + disallowApps?.forEach { + build.addDisallowedApplication(it) + } + build.setConfigureIntent( PendingIntent.getActivity( this, diff --git a/android/app/src/main/kotlin/com/network/proxy/plugin/PictureInPicturePlugin.kt b/android/app/src/main/kotlin/com/network/proxy/plugin/PictureInPicturePlugin.kt index 6bed5576..87d8c7c7 100644 --- a/android/app/src/main/kotlin/com/network/proxy/plugin/PictureInPicturePlugin.kt +++ b/android/app/src/main/kotlin/com/network/proxy/plugin/PictureInPicturePlugin.kt @@ -24,6 +24,8 @@ class PictureInPicturePlugin : AndroidFlutterPlugin() { var channel: MethodChannel? = null var proxyHost: String? = null var proxyPort: Int? = null + var allowApps: ArrayList? = null + var disallowApps: ArrayList? = null ///广播事件接受者 private val vpnBroadcastReceiver = object : BroadcastReceiver() { @@ -43,7 +45,15 @@ class PictureInPicturePlugin : AndroidFlutterPlugin() { if (isRunning) { activity.startService(ProxyVpnService.stopVpnIntent(activity)) } else { - activity.startService(ProxyVpnService.startVpnIntent(activity, proxyHost, proxyPort)) + activity.startService( + ProxyVpnService.startVpnIntent( + activity, + proxyHost, + proxyPort, + allowApps, + disallowApps + ) + ) } //设置画中画参数 @@ -67,6 +77,8 @@ class PictureInPicturePlugin : AndroidFlutterPlugin() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { proxyHost = call.argument("proxyHost") proxyPort = call.argument("proxyPort") + allowApps = call.argument>("allowApps") + disallowApps = call.argument>("disallowApps") val param = updatePictureInPictureParams(ProxyVpnService.isRunning) if (!registerBroadcast) { diff --git a/android/app/src/main/kotlin/com/network/proxy/plugin/VpnServicePlugin.kt b/android/app/src/main/kotlin/com/network/proxy/plugin/VpnServicePlugin.kt index 253c732c..5e715bcd 100644 --- a/android/app/src/main/kotlin/com/network/proxy/plugin/VpnServicePlugin.kt +++ b/android/app/src/main/kotlin/com/network/proxy/plugin/VpnServicePlugin.kt @@ -23,9 +23,10 @@ class VpnServicePlugin : AndroidFlutterPlugin() { val host = call.argument("proxyHost") val port = call.argument("proxyPort") val allowApps = call.argument>("allowApps") + val disallowApps = call.argument>("disallowApps") val prepareVpn = prepareVpn(host!!, port!!, allowApps) if (prepareVpn) { - startVpn(host, port, allowApps) + startVpn(host, port, allowApps, disallowApps) } result.success(prepareVpn) } @@ -39,8 +40,9 @@ class VpnServicePlugin : AndroidFlutterPlugin() { val host = call.argument("proxyHost") val port = call.argument("proxyPort") val allowApps = call.argument>("allowApps") + val disallowApps = call.argument>("disallowApps") stopVpn() - startVpn(host!!, port!!, allowApps) + startVpn(host!!, port!!, allowApps, disallowApps) } else -> { @@ -69,8 +71,13 @@ class VpnServicePlugin : AndroidFlutterPlugin() { /** * 启动vpn服务 */ - private fun startVpn(host: String, port: Int, allowApps: ArrayList?) { - val intent = ProxyVpnService.startVpnIntent(activity, host, port, allowApps) + private fun startVpn( + host: String, + port: Int, + allowApps: ArrayList?, + disallowApps: ArrayList? + ) { + val intent = ProxyVpnService.startVpnIntent(activity, host, port, allowApps, disallowApps) activity.startService(intent) } diff --git a/android/app/src/main/kotlin/com/network/proxy/vpn/util/ProcessInfoManager.kt b/android/app/src/main/kotlin/com/network/proxy/vpn/util/ProcessInfoManager.kt index f436c515..d2bd9f6c 100644 --- a/android/app/src/main/kotlin/com/network/proxy/vpn/util/ProcessInfoManager.kt +++ b/android/app/src/main/kotlin/com/network/proxy/vpn/util/ProcessInfoManager.kt @@ -23,7 +23,7 @@ class ProcessInfoManager private constructor() { } private val localPortUidMap = - CacheBuilder.newBuilder().maximumSize(10_000).expireAfterAccess(120, TimeUnit.SECONDS) + CacheBuilder.newBuilder().maximumSize(10_000).expireAfterAccess(60, TimeUnit.SECONDS) .build() private val appInfoCache = diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ce22f01d..c17fdf99 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -110,6 +110,7 @@ "domainBlacklist": "Domain Blacklist", "domainFilter": "Domain Filter", "appWhitelist": "App Whitelist", + "appBlacklist": "App Blacklist", "scanCode": "Scan Code Connect", "addBlacklist": "Add Filter Blacklist", "addWhitelist": "Add Filter Whitelist", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 4ccc04b1..dfbcd754 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -109,6 +109,7 @@ "domainWhitelist": "域名白名单", "domainBlacklist" :"域名黑名单", "appWhitelist": "应用白名单", + "appBlacklist": "应用黑名单", "domainFilter": "域名过滤", "scanCode": "扫码连接", "addBlacklist": "添加黑名单", diff --git a/lib/native/pip.dart b/lib/native/pip.dart index 30f03099..f1fe5c7c 100644 --- a/lib/native/pip.dart +++ b/lib/native/pip.dart @@ -28,9 +28,10 @@ class PictureInPicture { }); ///进入画中画模式 - static Future enterPictureInPictureMode(String host, int port) async { - final bool enterPictureInPictureMode = - await _channel.invokeMethod('enterPictureInPictureMode', {"proxyHost": host, "proxyPort": port}); + static Future enterPictureInPictureMode(String host, int port, + {List? appList, List? disallowApps}) async { + final bool enterPictureInPictureMode = await _channel.invokeMethod('enterPictureInPictureMode', + {"proxyHost": host, "proxyPort": port, "allowApps": appList, "disallowApps": disallowApps}); inPip = true; return enterPictureInPictureMode; diff --git a/lib/native/vpn.dart b/lib/native/vpn.dart index f86a5ace..1f3d9218 100644 --- a/lib/native/vpn.dart +++ b/lib/native/vpn.dart @@ -5,9 +5,9 @@ class Vpn { static bool isVpnStarted = false; //vpn是否已经启动 - static startVpn(String host, int port, {List? appList, bool? backgroundAudioEnable = true}) { - proxyVpnChannel.invokeMethod("startVpn", - {"proxyHost": host, "proxyPort": port, "allowApps": appList, "backgroundAudioEnable": backgroundAudioEnable}); + static startVpn(String host, int port, {List? appList, List? disallowApps}) { + proxyVpnChannel.invokeMethod( + "startVpn", {"proxyHost": host, "proxyPort": port, "allowApps": appList, "disallowApps": disallowApps}); isVpnStarted = true; } @@ -17,9 +17,9 @@ class Vpn { } //重启vpn - static restartVpn(String host, int port, {List? appList, bool? backgroundAudioEnable = true}) { - proxyVpnChannel.invokeMethod("restartVpn", - {"proxyHost": host, "proxyPort": port, "allowApps": appList, "backgroundAudioEnable": backgroundAudioEnable}); + static restartVpn(String host, int port, {List? appList, List? disallowApps}) { + proxyVpnChannel.invokeMethod( + "restartVpn", {"proxyHost": host, "proxyPort": port, "allowApps": appList, "disallowApps": disallowApps}); isVpnStarted = true; } diff --git a/lib/network/bin/configuration.dart b/lib/network/bin/configuration.dart index 1d3065c6..7aa12533 100644 --- a/lib/network/bin/configuration.dart +++ b/lib/network/bin/configuration.dart @@ -43,6 +43,9 @@ class Configuration { //白名单应用 List appWhitelist = []; + //应用黑名单 + List? appBlacklist; + //远程连接 不持久化保存 String? remoteHost; @@ -84,6 +87,7 @@ class Configuration { externalProxy = ProxyInfo.fromJson(config['externalProxy']); } appWhitelist = List.from(config['appWhitelist'] ?? []); + appBlacklist = config['appBlacklist'] == null ? null : List.from(config['appBlacklist']); HostFilter.whitelist.load(config['whitelist']); HostFilter.blacklist.load(config['blacklist']); } @@ -131,6 +135,7 @@ class Configuration { 'proxyPassDomains': proxyPassDomains, 'externalProxy': externalProxy?.toJson(), 'appWhitelist': appWhitelist, + 'appBlacklist': appBlacklist, 'historyCacheTime': historyCacheTime, 'whitelist': HostFilter.whitelist.toJson(), 'blacklist': HostFilter.blacklist.toJson(), diff --git a/lib/network/util/cache.dart b/lib/network/util/cache.dart index 70bf14e6..e0d6399a 100644 --- a/lib/network/util/cache.dart +++ b/lib/network/util/cache.dart @@ -15,6 +15,15 @@ class ExpiringCache { _expirationTimes[key] = Timer(duration, () => remove(key)); } + V? putIfAbsent(K key, V Function() ifAbsent) { + if (_cache.containsKey(key)) { + return _cache[key]; + } + final value = ifAbsent(); + set(key, value); + return value; + } + V? get(K key) { return _cache[key]; } diff --git a/lib/ui/mobile/menu/drawer.dart b/lib/ui/mobile/menu/drawer.dart index 7132caf2..7c944981 100644 --- a/lib/ui/mobile/menu/drawer.dart +++ b/lib/ui/mobile/menu/drawer.dart @@ -14,7 +14,7 @@ import 'package:network_proxy/ui/configuration.dart'; import 'package:network_proxy/ui/mobile/menu/preference.dart'; import 'package:network_proxy/ui/mobile/request/favorite.dart'; import 'package:network_proxy/ui/mobile/request/history.dart'; -import 'package:network_proxy/ui/mobile/setting/app_whitelist.dart'; +import 'package:network_proxy/ui/mobile/setting/app_filter.dart'; import 'package:network_proxy/ui/mobile/setting/filter.dart'; import 'package:network_proxy/ui/mobile/setting/request_block.dart'; import 'package:network_proxy/ui/mobile/setting/request_rewrite.dart'; @@ -153,6 +153,12 @@ class FilterMenu extends StatelessWidget { title: Text(localizations.appWhitelist), trailing: const Icon(Icons.arrow_right), onTap: () => navigator(context, AppWhitelist(proxyServer: proxyServer))), + Platform.isIOS + ? const SizedBox() + : ListTile( + title: Text(localizations.appBlacklist), + trailing: const Icon(Icons.arrow_right), + onTap: () => navigator(context, AppBlacklist(proxyServer: proxyServer))), ]))); } } diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index 4e6d7c5d..fcea3311 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -66,7 +66,8 @@ class MobileHomeState extends State implements EventListener, Li } return PictureInPicture.enterPictureInPictureMode( - Platform.isAndroid ? await localIp() : "127.0.0.1", proxyServer.port); + Platform.isAndroid ? await localIp() : "127.0.0.1", proxyServer.port, + appList: proxyServer.configuration.appWhitelist, disallowApps: proxyServer.configuration.appBlacklist); } return false; } @@ -207,8 +208,8 @@ class MobileHomeState extends State implements EventListener, Li serverLaunch: false, onStart: () async { Vpn.startVpn(Platform.isAndroid ? await localIp() : "127.0.0.1", proxyServer.port, - backgroundAudioEnable: widget.appConfiguration.iosVpnBackgroundAudioEnable, - appList: proxyServer.configuration.appWhitelist); + appList: proxyServer.configuration.appWhitelist, + disallowApps: proxyServer.configuration.appBlacklist); }, onStop: () => Vpn.stopVpn())), ); diff --git a/lib/ui/mobile/request/request.dart b/lib/ui/mobile/request/request.dart index e0b17fd9..70cee0ea 100644 --- a/lib/ui/mobile/request/request.dart +++ b/lib/ui/mobile/request/request.dart @@ -9,6 +9,7 @@ import 'package:network_proxy/network/bin/server.dart'; import 'package:network_proxy/network/host_port.dart'; import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/network/http_client.dart'; +import 'package:network_proxy/network/util/cache.dart'; import 'package:network_proxy/storage/favorites.dart'; import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/content/panel.dart'; @@ -42,6 +43,8 @@ class RequestRow extends StatefulWidget { } class RequestRowState extends State { + static ExpiringCache imageCache = ExpiringCache(const Duration(minutes: 5)); + late HttpRequest request; HttpResponse? response; bool selected = false; @@ -114,8 +117,10 @@ class RequestRowState extends State { } //如果有缓存图标直接返回图标 - if(request.processInfo!.hasCacheIcon){ - return Image.memory(request.processInfo!.cacheIcon!, width: 40); + if (request.processInfo!.hasCacheIcon) { + return imageCache.putIfAbsent(request.processInfo!.id, () { + return Image.memory(request.processInfo!.cacheIcon!, width: 40, gaplessPlayback: true); + }); } return FutureBuilder( diff --git a/lib/ui/mobile/setting/app_whitelist.dart b/lib/ui/mobile/setting/app_filter.dart similarity index 63% rename from lib/ui/mobile/setting/app_whitelist.dart rename to lib/ui/mobile/setting/app_filter.dart index e04db695..3f682d96 100644 --- a/lib/ui/mobile/setting/app_whitelist.dart +++ b/lib/ui/mobile/setting/app_filter.dart @@ -6,7 +6,6 @@ import 'package:network_proxy/native/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 { @@ -37,8 +36,7 @@ class _AppWhitelistState extends State { configuration.flushConfig(); if (Vpn.isVpnStarted) { Vpn.restartVpn("127.0.0.1", widget.proxyServer.port, - appList: configuration.appWhitelist, - backgroundAudioEnable: AppConfiguration.current?.iosVpnBackgroundAudioEnable); + appList: configuration.appWhitelist, disallowApps: configuration.appBlacklist); } } super.dispose(); @@ -126,6 +124,118 @@ class _AppWhitelistState extends State { } } +class AppBlacklist extends StatefulWidget { + final ProxyServer proxyServer; + + const AppBlacklist({super.key, required this.proxyServer}); + + @override + State createState() => _AppBlacklistState(); +} + +class _AppBlacklistState extends State { + late Configuration configuration; + + bool changed = false; + + AppLocalizations get localizations => AppLocalizations.of(context)!; + + @override + void initState() { + super.initState(); + configuration = widget.proxyServer.configuration; + } + + @override + void dispose() { + if (changed) { + configuration.flushConfig(); + if (Vpn.isVpnStarted) { + Vpn.restartVpn("127.0.0.1", widget.proxyServer.port, + appList: configuration.appWhitelist, disallowApps: configuration.appBlacklist); + } + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + bool isCN = Localizations.localeOf(context) == const Locale.fromSubtags(languageCode: 'zh'); + var appBlacklist = >[]; + for (var element in configuration.appBlacklist ?? []) { + appBlacklist.add(InstalledApps.getAppInfo(element).catchError((e) { + return AppInfo(name: isCN ? "未知应用" : "Unknown app", packageName: element); + })); + } + + return Scaffold( + appBar: AppBar( + title: Text(localizations.appBlacklist, style: const TextStyle(fontSize: 16)), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + //添加 + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) => const InstalledAppsWidget())) + .then((value) { + if (value != null) { + if (configuration.appBlacklist?.contains(value) == true) { + return; + } + setState(() { + configuration.appBlacklist ??= []; + configuration.appBlacklist?.add(value); + changed = true; + }); + } + }); + }, + ), + ], + ), + body: FutureBuilder( + future: Future.wait(appBlacklist), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + if (snapshot.data!.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Text(localizations.emptyData, style: const TextStyle(color: Colors.grey))), + ); + } + + return ListView.builder( + itemCount: snapshot.data!.length, + itemBuilder: (BuildContext context, int index) { + AppInfo appInfo = snapshot.data![index]; + return ListTile( + leading: appInfo.icon == null ? const Icon(Icons.question_mark) : Image.memory(appInfo.icon!), + title: Text(appInfo.name ?? ""), + subtitle: Text(appInfo.packageName ?? ""), + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + //删除 + setState(() { + configuration.appBlacklist?.remove(appInfo.packageName); + changed = true; + }); + }, + ), + ); + }); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }), + ); + } +} + ///已安装的app列表 class InstalledAppsWidget extends StatefulWidget { const InstalledAppsWidget({super.key}); diff --git a/lib/ui/mobile/widgets/pip.dart b/lib/ui/mobile/widgets/pip.dart index 9e5d272a..12c722de 100644 --- a/lib/ui/mobile/widgets/pip.dart +++ b/lib/ui/mobile/widgets/pip.dart @@ -130,7 +130,9 @@ class _PictureInPictureState extends State { tooltip: localizations.windowMode, onPressed: () async { PictureInPicture.enterPictureInPictureMode( - Platform.isAndroid ? await localIp() : "127.0.0.1", widget.proxyServer.port); + Platform.isAndroid ? await localIp() : "127.0.0.1", widget.proxyServer.port, + appList: widget.proxyServer.configuration.appWhitelist, + disallowApps: widget.proxyServer.configuration.appBlacklist); }, icon: const Icon(Icons.picture_in_picture_alt))), )