From 0a63c055c48b8312e74255fbcd5730c22398da30 Mon Sep 17 00:00:00 2001 From: wanghongenpin Date: Sat, 23 Dec 2023 22:51:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=8F=E7=AA=97=E5=8F=A3=E6=B8=85=E7=90=86?= =?UTF-8?q?=E6=8C=89=E9=92=AE=EF=BC=8C=E9=80=80=E5=87=BA=E4=BA=8C=E6=AC=A1?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/network/proxy/MainActivity.kt | 16 +- .../proxy/plugin/AppLifecyclePlugin.kt | 4 +- .../proxy/plugin/PictureInPicturePlugin.kt | 49 +++++-- .../network/proxy/plugin/VpnServicePlugin.kt | 1 + .../kotlin/com/network/proxy/vpn/util/TLS.kt | 12 ++ lib/native/app_lifecycle.dart | 57 ++++++-- lib/native/pip.dart | 11 +- lib/network/network.dart | 4 +- lib/ui/mobile/mobile.dart | 138 +++++++++++------- lib/ui/mobile/request/list.dart | 7 +- lib/ui/mobile/request/request.dart | 3 +- lib/utils/lang.dart | 16 ++ 12 files changed, 226 insertions(+), 92 deletions(-) diff --git a/android/app/src/main/kotlin/com/network/proxy/MainActivity.kt b/android/app/src/main/kotlin/com/network/proxy/MainActivity.kt index 4b11b64..ab28936 100644 --- a/android/app/src/main/kotlin/com/network/proxy/MainActivity.kt +++ b/android/app/src/main/kotlin/com/network/proxy/MainActivity.kt @@ -1,7 +1,7 @@ package com.network.proxy -import android.app.PictureInPictureParams import android.content.Intent +import android.content.res.Configuration import android.net.VpnService import android.os.Bundle import com.network.proxy.plugin.AppLifecyclePlugin @@ -29,9 +29,12 @@ class MainActivity : FlutterActivity() { lifecycleChannel.onUserLeaveHint() } - override fun onResume() { - super.onResume() - lifecycleChannel.onResume() + override fun onPictureInPictureModeChanged( + isInPictureInPictureMode: Boolean, + newConfig: Configuration? + ) { + lifecycleChannel.onPictureInPictureModeChanged(isInPictureInPictureMode) + super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) } /** @@ -58,4 +61,9 @@ class MainActivity : FlutterActivity() { super.onActivityResult(requestCode, resultCode, data) } + override fun onDestroy() { + activity.startService(ProxyVpnService.stopVpnIntent(activity)) + super.onDestroy() + } + } diff --git a/android/app/src/main/kotlin/com/network/proxy/plugin/AppLifecyclePlugin.kt b/android/app/src/main/kotlin/com/network/proxy/plugin/AppLifecyclePlugin.kt index f609a4c..1115806 100644 --- a/android/app/src/main/kotlin/com/network/proxy/plugin/AppLifecyclePlugin.kt +++ b/android/app/src/main/kotlin/com/network/proxy/plugin/AppLifecyclePlugin.kt @@ -19,8 +19,8 @@ class AppLifecyclePlugin : AndroidFlutterPlugin() { channel?.invokeMethod("onUserLeaveHint", null) } - fun onResume() { - channel?.invokeMethod("onResume", null) + fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) { + channel?.invokeMethod("onPictureInPictureModeChanged", isInPictureInPictureMode) } } \ No newline at end of file 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 2d64421..b645699 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 @@ -21,15 +21,21 @@ import androidx.core.content.ContextCompat */ class PictureInPicturePlugin : AndroidFlutterPlugin() { private var registerBroadcast = false + var channel: MethodChannel? = null ///广播事件接受者 private val vpnBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { Log.d("com.network.proxy", "onReceive ${intent?.action}") - if (context == null || intent?.action != VPN_ACTION) { + if (context == null || (intent?.action != VPN_ACTION && intent?.action != CLEAN_ACTION)) { return } + if (intent.action == CLEAN_ACTION) { + channel?.invokeMethod("cleanSession", null) + return + } + val isRunning = ProxyVpnService.isRunning if (isRunning) { @@ -48,11 +54,12 @@ class PictureInPicturePlugin : AndroidFlutterPlugin() { companion object { const val CHANNEL = "com.proxy/pictureInPicture" const val VPN_ACTION = "VPN_ACTION" + const val CLEAN_ACTION = "CLEAN_ACTION" } override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - val channel = MethodChannel(binding.binaryMessenger, CHANNEL) - channel.setMethodCallHandler { call, result -> + channel = MethodChannel(binding.binaryMessenger, CHANNEL) + channel!!.setMethodCallHandler { call, result -> when (call.method) { "enterPictureInPictureMode" -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -63,7 +70,10 @@ class PictureInPicturePlugin : AndroidFlutterPlugin() { ContextCompat.registerReceiver( activity, vpnBroadcastReceiver, - IntentFilter(VPN_ACTION), + IntentFilter().apply { + addAction(VPN_ACTION) + addAction(CLEAN_ACTION) + }, ContextCompat.RECEIVER_NOT_EXPORTED ) } @@ -84,9 +94,9 @@ class PictureInPicturePlugin : AndroidFlutterPlugin() { val params = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { PictureInPictureParams.Builder() - .setAspectRatio(Rational(8, 19)) + .setAspectRatio(Rational(9, 19)) .apply { - setActions(listOf(action(isRunning))) //vpn服务运行中,显示停止按钮 + setActions(actions(isRunning)) //vpn服务运行中,显示停止按钮 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { setSeamlessResizeEnabled(false) } @@ -100,7 +110,7 @@ class PictureInPicturePlugin : AndroidFlutterPlugin() { } //停止vpn服务 RemoteAction - private fun action(isRunning: Boolean): RemoteAction { + private fun actions(isRunning: Boolean): List { val pIntent: PendingIntent = PendingIntent.getBroadcast( activity, if (isRunning) 0 else 1, @@ -108,13 +118,28 @@ class PictureInPicturePlugin : AndroidFlutterPlugin() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT ) + val cleanIntent: PendingIntent = PendingIntent.getBroadcast( + activity, + 2, + Intent(CLEAN_ACTION), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT + ) + //vpn服务运行中,显示停止按钮 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - return RemoteAction( - Icon.createWithResource( - this@PictureInPicturePlugin.activity, - if (isRunning) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play - ), "Proxy", "Proxy", pIntent + return listOf( + RemoteAction( + Icon.createWithResource( + this@PictureInPicturePlugin.activity, + if (isRunning) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play + ), "Proxy", "Proxy", pIntent + ), + RemoteAction( + Icon.createWithResource( + this@PictureInPicturePlugin.activity, + android.R.drawable.ic_menu_delete + ), "Clean", "Clean", cleanIntent + ) ) } else { throw RuntimeException("action error") 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 ab9de71..c7a6e86 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 @@ -27,6 +27,7 @@ class VpnServicePlugin : AndroidFlutterPlugin() { "stopVpn" -> { stopVpn() + result.success(null) } "restartVpn" -> { diff --git a/android/app/src/main/kotlin/com/network/proxy/vpn/util/TLS.kt b/android/app/src/main/kotlin/com/network/proxy/vpn/util/TLS.kt index c1e2939..ec70303 100644 --- a/android/app/src/main/kotlin/com/network/proxy/vpn/util/TLS.kt +++ b/android/app/src/main/kotlin/com/network/proxy/vpn/util/TLS.kt @@ -7,6 +7,18 @@ import kotlin.math.min class TLS { companion object { + /** + * 判断是否是TLS Client Hello + */ + fun isTLSClientHello(packetData: ByteBuffer): Boolean { + if (packetData.remaining() < 43) return false + val position = packetData.position() + val data = packetData.array() + if (data[position].toInt() != 0x16 /* handshake */) return false + if (data[1 + position].toInt() != 0x03) return false + return if (data[5 + position].toInt() != 0x01) false else data[9 + position].toInt() == 0x03 && data[10 + position] >= 0x00 && data[1 + position] <= 0x03 + } + /** * 从TLS Client Hello 解析域名 */ diff --git a/lib/native/app_lifecycle.dart b/lib/native/app_lifecycle.dart index 161240a..2afc650 100644 --- a/lib/native/app_lifecycle.dart +++ b/lib/native/app_lifecycle.dart @@ -1,34 +1,59 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:network_proxy/network/util/logger.dart'; -abstract interface class AppLifecycleListener { - void onUserLeaveHint(AppLifecycleState state); +abstract interface class LifecycleListener { + void onUserLeaveHint() {} - void onDetached(AppLifecycleState state); + void onPictureInPictureModeChanged(bool isInPictureInPictureMode) {} } class AppLifecycleBinding { static const MethodChannel _methodChannel = MethodChannel('com.proxy/appLifecycle'); - static bool _initialized = false; - static ensureInitialized() { - if (_initialized) { - return; - } - - //注册方法 - _methodChannel.setMethodCallHandler(_methodCallHandler); - _initialized = true; + //单例对象 + static AppLifecycleBinding get instance { + _instance ??= AppLifecycleBinding._(); + return _instance!; } - static Future _methodCallHandler(MethodCall call) async { + final List _listeners = []; + + static AppLifecycleBinding? _instance; + + AppLifecycleBinding._() { + //注册方法 + _methodChannel.setMethodCallHandler(_methodCallHandler); + } + + static AppLifecycleBinding ensureInitialized() { + return AppLifecycleBinding.instance; + } + + addListener(LifecycleListener listener) { + if (_listeners.contains(listener)) return; + _listeners.add(listener); + } + + removeListener(LifecycleListener listener) { + _listeners.remove(listener); + } + + Future _methodCallHandler(MethodCall call) async { + logger.d("AppLifecycle methodCallHandler ${call.method}"); switch (call.method) { case 'appDetached': await WidgetsBinding.instance.handleRequestAppExit(); break; - case 'userLeaveHint': - print("userLeaveHint"); - // WidgetsBinding.instance.handleRequestAppExit(); + case 'onUserLeaveHint': + for (var listener in _listeners) { + listener.onUserLeaveHint(); + } + break; + case 'onPictureInPictureModeChanged': + for (var listener in _listeners) { + listener.onPictureInPictureModeChanged(call.arguments); + } break; } return Future.value(); diff --git a/lib/native/pip.dart b/lib/native/pip.dart index 9881d5e..f92ee23 100644 --- a/lib/native/pip.dart +++ b/lib/native/pip.dart @@ -1,10 +1,19 @@ import 'dart:io'; import 'package:flutter/services.dart'; +import 'package:network_proxy/network/util/logger.dart'; +import 'package:network_proxy/ui/mobile/mobile.dart'; ///画中画 class PictureInPicture { - static const MethodChannel _channel = MethodChannel('com.proxy/pictureInPicture'); + static final MethodChannel _channel = const MethodChannel('com.proxy/pictureInPicture') + ..setMethodCallHandler((call) async { + logger.d("pictureInPicture MethodCallHandler ${call.method}"); + if (call.method == 'cleanSession') { + MobileHomeState.requestStateKey.currentState?.clean(); + } + return Future.value(); + }); ///进入画中画模式 static Future enterPictureInPictureMode() async { diff --git a/lib/network/network.dart b/lib/network/network.dart index 70079cb..4f5c2e1 100644 --- a/lib/network/network.dart +++ b/lib/network/network.dart @@ -112,9 +112,11 @@ class Network { host: hostAndPort?.host, onBadCertificate: (certificate) => true); } - // var selectedProtocol = remoteChannel?.selectedProtocol; //ssl自签证书 var certificate = await CertificateManager.getCertificateContext(hostAndPort!.host); + // var selectedProtocol = remoteChannel?.selectedProtocol; + // if (selectedProtocol != null) certificate.setAlpnProtocols([selectedProtocol], true); + //服务端等待客户端ssl握手 channel.secureSocket = await SecureSocket.secureServer(channel.socket, certificate, bufferedData: data); } catch (error, trace) { diff --git a/lib/ui/mobile/mobile.dart b/lib/ui/mobile/mobile.dart index 1d33dd7..edb4edc 100644 --- a/lib/ui/mobile/mobile.dart +++ b/lib/ui/mobile/mobile.dart @@ -2,6 +2,9 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_toastr/flutter_toastr.dart'; +import 'package:network_proxy/native/app_lifecycle.dart'; import 'package:network_proxy/native/pip.dart'; import 'package:network_proxy/native/vpn.dart'; import 'package:network_proxy/network/bin/configuration.dart'; @@ -32,23 +35,34 @@ class MobileHomePage extends StatefulWidget { } } -class MobileHomeState extends State with WidgetsBindingObserver implements EventListener { - final GlobalKey requestStateKey = GlobalKey(); +class MobileHomeState extends State implements EventListener, LifecycleListener { + static final GlobalKey requestStateKey = GlobalKey(); late ProxyServer proxyServer; ValueNotifier desktop = ValueNotifier(RemoteModel(connect: false)); @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.inactive && Vpn.isVpnStarted) { + void onUserLeaveHint() { + if (Vpn.isVpnStarted && !pictureInPictureNotifier.value) { if (desktop.value.connect || !Platform.isAndroid || !widget.configuration.smallWindow) { return; } - PictureInPicture.enterPictureInPictureMode().then((value) => pictureInPictureNotifier.value = value); + PictureInPicture.enterPictureInPictureMode(); + } + } + + @override + onPictureInPictureModeChanged(bool isInPictureInPictureMode) { + if (isInPictureInPictureMode && !pictureInPictureNotifier.value) { + while (Navigator.canPop(context)) { + Navigator.pop(context); + } + pictureInPictureNotifier.value = true; + return; } - if (state == AppLifecycleState.resumed && pictureInPictureNotifier.value) { + if (!isInPictureInPictureMode && pictureInPictureNotifier.value) { Vpn.isRunning().then((value) { Vpn.isVpnStarted = value; pictureInPictureNotifier.value = false; @@ -77,7 +91,7 @@ class MobileHomeState extends State with WidgetsBindingObserver @override void initState() { super.initState(); - WidgetsBinding.instance.addObserver(this); + AppLifecycleBinding.instance.addListener(this); proxyServer = ProxyServer(widget.configuration); proxyServer.addListener(this); proxyServer.start(); @@ -102,59 +116,77 @@ class MobileHomeState extends State with WidgetsBindingObserver @override void dispose() { desktop.dispose(); - WidgetsBinding.instance.removeObserver(this); + AppLifecycleBinding.instance.removeListener(this); super.dispose(); } + int exitTime = 0; + @override Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: pictureInPictureNotifier, - builder: (context, pip, _) { - if (pip) { - return Scaffold(body: RequestListWidget(key: requestStateKey, proxyServer: proxyServer)); + return PopScope( + canPop: false, + onPopInvoked: (d) { + if (DateTime.now().millisecondsSinceEpoch - exitTime > 2000) { + exitTime = DateTime.now().millisecondsSinceEpoch; + FlutterToastr.show("再按一次退出程序", context, rootNavigator: true, duration: FlutterToastr.lengthLong); + return; } - return Scaffold( - appBar: AppBar( - title: MobileSearch(onSearch: (val) { - requestStateKey.currentState?.search(val); - }), - actions: [ - IconButton( - tooltip: "清理", - icon: const Icon(Icons.cleaning_services_outlined), - onPressed: () => requestStateKey.currentState?.clean()), - const SizedBox(width: 2), - MoreMenu(proxyServer: proxyServer, desktop: desktop), - const SizedBox(width: 10) - ]), - drawer: DrawerWidget(proxyServer: proxyServer), - floatingActionButton: FloatingActionButton( - onPressed: null, - child: Center( - child: futureWidget(localIp(), (data) { - SocketLaunch.started = Vpn.isVpnStarted; - return SocketLaunch( - proxyServer: proxyServer, - size: 36, - startup: false, - serverLaunch: false, - onStart: () => Vpn.startVpn(Platform.isAndroid ? data : "127.0.0.1", proxyServer.port, - proxyServer.configuration.appWhitelist), - onStop: () => Vpn.stopVpn()); - })), - ), - body: ValueListenableBuilder( - valueListenable: desktop, - builder: (context, value, _) { - return Column(children: [ - value.connect ? remoteConnect(value) : const SizedBox(), - Expanded(child: RequestListWidget(key: requestStateKey, proxyServer: proxyServer)) - ]); - }), - ); - }); + //退出程序 + SystemNavigator.pop(); + }, + child: ValueListenableBuilder( + valueListenable: pictureInPictureNotifier, + builder: (context, pip, _) { + if (pip) { + return Scaffold(body: RequestListWidget(key: requestStateKey, proxyServer: proxyServer)); + } + + return Scaffold( + appBar: appBar(), + drawer: DrawerWidget(proxyServer: proxyServer), + floatingActionButton: _floatingActionButton(), + body: ValueListenableBuilder( + valueListenable: desktop, + builder: (context, value, _) { + return Column(children: [ + value.connect ? remoteConnect(value) : const SizedBox(), + Expanded(child: RequestListWidget(key: requestStateKey, proxyServer: proxyServer)) + ]); + }), + ); + })); + } + + AppBar appBar() { + return AppBar(title: MobileSearch(onSearch: (val) => requestStateKey.currentState?.search(val)), actions: [ + IconButton( + tooltip: "清理", + icon: const Icon(Icons.cleaning_services_outlined), + onPressed: () => requestStateKey.currentState?.clean()), + const SizedBox(width: 2), + MoreMenu(proxyServer: proxyServer, desktop: desktop), + const SizedBox(width: 10) + ]); + } + + FloatingActionButton _floatingActionButton() { + return FloatingActionButton( + onPressed: null, + child: Center( + child: futureWidget(localIp(), (data) { + SocketLaunch.started = Vpn.isVpnStarted; + return SocketLaunch( + proxyServer: proxyServer, + size: 36, + startup: false, + serverLaunch: false, + onStart: () => Vpn.startVpn( + Platform.isAndroid ? data : "127.0.0.1", proxyServer.port, proxyServer.configuration.appWhitelist), + onStop: () => Vpn.stopVpn()); + })), + ); } showUpgradeNotice() { diff --git a/lib/ui/mobile/request/list.dart b/lib/ui/mobile/request/list.dart index 456940e..31ffce4 100644 --- a/lib/ui/mobile/request/list.dart +++ b/lib/ui/mobile/request/list.dart @@ -12,6 +12,7 @@ import 'package:network_proxy/network/http/http.dart'; import 'package:network_proxy/ui/desktop/left/model/search_model.dart'; import 'package:network_proxy/ui/mobile/request/request.dart'; import 'package:network_proxy/ui/ui_configuration.dart'; +import 'package:network_proxy/utils/lang.dart'; class RequestListWidget extends StatefulWidget { final ProxyServer proxyServer; @@ -55,11 +56,13 @@ class RequestListState extends State { return ListView.separated( padding: const EdgeInsets.only(left: 2), itemCount: container.length, - separatorBuilder: (context, index) => const Divider(thickness: 0.2, height: 0.5), + separatorBuilder: (context, index) => const Divider(thickness: 0.3, height: 0.5), itemBuilder: (context, index) { return Text.rich( overflow: TextOverflow.ellipsis, - TextSpan(text: container[container.length - index - 1].requestUrl, style: const TextStyle(fontSize: 8)), + TextSpan( + text: container[container.length - index - 1].requestUrl.fixAutoLines(), + style: const TextStyle(fontSize: 9)), maxLines: 2); }); } diff --git a/lib/ui/mobile/request/request.dart b/lib/ui/mobile/request/request.dart index 403a2e6..3326a11 100644 --- a/lib/ui/mobile/request/request.dart +++ b/lib/ui/mobile/request/request.dart @@ -11,6 +11,7 @@ import 'package:network_proxy/ui/component/utils.dart'; import 'package:network_proxy/ui/content/panel.dart'; import 'package:network_proxy/ui/mobile/request/request_editor.dart'; import 'package:network_proxy/utils/curl.dart'; +import 'package:network_proxy/utils/lang.dart'; ///请求行 class RequestRow extends StatefulWidget { @@ -53,7 +54,7 @@ class RequestRowState extends State { @override Widget build(BuildContext context) { - var title = '${request.method.name} ${widget.displayDomain ? request.requestUrl : request.path()}'; + var title = Strings.autoLineString('${request.method.name} ${widget.displayDomain ? request.requestUrl : request.path()}'); var time = formatDate(request.requestTime, [HH, ':', nn, ':', ss]); var contentType = response?.contentType.name.toUpperCase() ?? ''; diff --git a/lib/utils/lang.dart b/lib/utils/lang.dart index 73aee32..0fa2f14 100644 --- a/lib/utils/lang.dart +++ b/lib/utils/lang.dart @@ -1,4 +1,5 @@ import 'package:date_format/date_format.dart'; +import 'package:flutter/material.dart'; extension ListFirstWhere on Iterable { T? firstWhereOrNull(bool Function(T) test) { @@ -50,6 +51,21 @@ class Strings { } return str; } + + ///防止文字自动换行 + static String autoLineString(String str) { + return str.fixAutoLines(); + } +} + +/// 防止文字自动换行 +/// 当中英文混合,或者中文与数字或者特殊符号,或则英文单词时,文本会被自动换行, +/// 这样会导致,换行时上一行可能会留很大的空白区域 +/// 把每个字符插入一个0宽的字符, \u{200B} +extension FixAutoLines on String { + String fixAutoLines() { + return Characters(this).join('\u{200B}'); + } } class Pair {