diff --git a/.gitignore b/.gitignore index 79fa1c2..94cd3ca 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ .packages .pub/ +pubspec.lock build/ android/src/main/libs diff --git a/android/build.gradle b/android/build.gradle index 20048bb..bb79cde 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group 'io.xdea.flutter_vpn' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.4.10' + ext.kotlin_version = '1.4.21' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:4.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // For downloading prebuilt libs classpath 'de.undercouch:gradle-download-task:4.0.2' diff --git a/android/src/main/java/org/strongswan/android/logic/CharonVpnService.java b/android/src/main/java/org/strongswan/android/logic/CharonVpnService.java index 67547ce..b808560 100644 --- a/android/src/main/java/org/strongswan/android/logic/CharonVpnService.java +++ b/android/src/main/java/org/strongswan/android/logic/CharonVpnService.java @@ -142,9 +142,11 @@ public int onStartCommand(Intent intent, int flags, int startId) { profile = new VpnProfile(); profile.setId(1); profile.setUUID(UUID.randomUUID()); - profile.setName(bundle.getString("Address")); - profile.setGateway(bundle.getString("Address")); - profile.setUsername(bundle.getString("UserName")); + profile.setName(bundle.getString("Name")); + profile.setGateway(bundle.getString("Server")); + if (bundle.containsKey("Port")) + profile.setPort(bundle.getInt("Port")); + profile.setUsername(bundle.getString("Username")); profile.setPassword(bundle.getString("Password")); profile.setMTU(bundle.getInt("MTU")); profile.setVpnType(VpnType.fromIdentifier(bundle.getString("VpnType"))); diff --git a/android/src/main/kotlin/io/xdea/flutter_vpn/FlutterVpnPlugin.kt b/android/src/main/kotlin/io/xdea/flutter_vpn/FlutterVpnPlugin.kt index efaa5b0..d26b40d 100644 --- a/android/src/main/kotlin/io/xdea/flutter_vpn/FlutterVpnPlugin.kt +++ b/android/src/main/kotlin/io/xdea/flutter_vpn/FlutterVpnPlugin.kt @@ -36,123 +36,126 @@ import io.flutter.plugin.common.PluginRegistry /** FlutterVpnPlugin */ class FlutterVpnPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { - private lateinit var activityBinding: ActivityPluginBinding - - /// The MethodChannel that will the communication between Flutter and native Android - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private lateinit var channel: MethodChannel - private lateinit var eventChannel: EventChannel - - private var vpnStateService: VpnStateService? = null - private val _serviceConnection = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName, service: IBinder) { - vpnStateService = (service as VpnStateService.LocalBinder).service - VpnStateHandler.vpnStateService = vpnStateService - vpnStateService?.registerListener(VpnStateHandler) + private lateinit var activityBinding: ActivityPluginBinding + + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + private lateinit var eventChannel: EventChannel + + private var vpnStateService: VpnStateService? = null + private val _serviceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, service: IBinder) { + vpnStateService = (service as VpnStateService.LocalBinder).service + VpnStateHandler.vpnStateService = vpnStateService + vpnStateService?.registerListener(VpnStateHandler) + } + + override fun onServiceDisconnected(name: ComponentName) { + vpnStateService = null + VpnStateHandler.vpnStateService = null + } + } + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + // Load charon bridge + System.loadLibrary("androidbridge") + + // Register method channel. + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_vpn") + channel.setMethodCallHandler(this); + + // Register event channel to handle state change. + eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "flutter_vpn_states") + eventChannel.setStreamHandler(VpnStateHandler) + + flutterPluginBinding.applicationContext.bindService( + Intent(flutterPluginBinding.applicationContext, VpnStateService::class.java), + _serviceConnection, + Service.BIND_AUTO_CREATE + ) + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + eventChannel.setStreamHandler(null) } - override fun onServiceDisconnected(name: ComponentName) { - vpnStateService = null - VpnStateHandler.vpnStateService = null + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activityBinding = binding } - } - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - // Load charon bridge - System.loadLibrary("androidbridge") - - // Register method channel. - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_vpn") - channel.setMethodCallHandler(this); - - // Register event channel to handle state change. - eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "flutter_vpn_states") - eventChannel.setStreamHandler(VpnStateHandler) - - flutterPluginBinding.applicationContext.bindService( - Intent(flutterPluginBinding.applicationContext, VpnStateService::class.java), - _serviceConnection, - Service.BIND_AUTO_CREATE - ) - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - eventChannel.setStreamHandler(null) - } - - - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activityBinding = binding - } - - override fun onDetachedFromActivity() { - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activityBinding = binding - } - - override fun onDetachedFromActivityForConfigChanges() { - } - - override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { - when (call.method) { - "prepare" -> { - val intent = VpnService.prepare(activityBinding.activity.applicationContext) - if (intent != null) { - var listener: PluginRegistry.ActivityResultListener? = null - listener = PluginRegistry.ActivityResultListener { req, res, _ -> - if (req == 0 && res == RESULT_OK) { - result.success(true) - } else { - result.success(false) + + override fun onDetachedFromActivity() { + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activityBinding = binding + } + + override fun onDetachedFromActivityForConfigChanges() { + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + when (call.method) { + "prepare" -> { + val intent = VpnService.prepare(activityBinding.activity.applicationContext) + if (intent != null) { + var listener: PluginRegistry.ActivityResultListener? = null + listener = PluginRegistry.ActivityResultListener { req, res, _ -> + if (req == 0 && res == RESULT_OK) { + result.success(true) + } else { + result.success(false) + } + listener?.let { activityBinding.removeActivityResultListener(it) }; + true + } + activityBinding.addActivityResultListener(listener) + activityBinding.activity.startActivityForResult(intent, 0) + } else { + // If intent is null, already prepared + result.success(true) + } } - listener?.let { activityBinding.removeActivityResultListener(it) }; - true - } - activityBinding.addActivityResultListener(listener) - activityBinding.activity.startActivityForResult(intent, 0) - } else { - // If intent is null, already prepared - result.success(true) - } - } - "prepared" -> { - val intent = VpnService.prepare(activityBinding.activity.applicationContext) - result.success(intent == null) - } - "connect" -> { - val intent = VpnService.prepare(activityBinding.activity.applicationContext) - if (intent != null) { - // Not prepared yet - result.success(false) - return + "prepared" -> { + val intent = VpnService.prepare(activityBinding.activity.applicationContext) + result.success(intent == null) + } + "connect" -> { + val intent = VpnService.prepare(activityBinding.activity.applicationContext) + if (intent != null) { + // Not prepared yet + result.success(false) + return + } + + val map = call.arguments as HashMap<*, *> + + val profileInfo = Bundle() + profileInfo.putString("VpnType", "ikev2-eap") + profileInfo.putString("Name", map["name"] as String) + profileInfo.putString("Server", map["server"] as String) + profileInfo.putString("Username", map["username"] as String) + profileInfo.putString("Password", map["password"] as String) + profileInfo.putInt("MTU", map["mtu"] as? Int ?: 1400) + if (map.containsKey("port")) + profileInfo.putInt("Port", map["port"] as Int) + + vpnStateService?.connect(profileInfo, true) + result.success(true) + } + "getCurrentState" -> { + if (vpnStateService?.errorState != VpnStateService.ErrorState.NO_ERROR) + result.success(4) + else + result.success(vpnStateService?.state?.ordinal) + } + "getCharonErrorState" -> result.success(vpnStateService?.errorState?.ordinal) + "disconnect" -> vpnStateService?.disconnect() + else -> result.notImplemented() } - - val map = call.arguments as HashMap - - val profileInfo = Bundle() - profileInfo.putString("Address", map["address"]) - profileInfo.putString("UserName", map["username"]) - profileInfo.putString("Password", map["password"]) - profileInfo.putString("VpnType", "ikev2-eap") - profileInfo.putInt("MTU", map["mtu"]?.toInt() ?: 1400) - - vpnStateService?.connect(profileInfo, true) - result.success(true) - } - "getCurrentState" -> { - if (vpnStateService?.errorState != VpnStateService.ErrorState.NO_ERROR) - result.success(4) - else - result.success(vpnStateService?.state?.ordinal) - } - "getCharonErrorState" -> result.success(vpnStateService?.errorState?.ordinal) - "disconnect" -> vpnStateService?.disconnect() - else -> result.notImplemented() } - } } diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 91a5882..397d7eb 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -49,6 +49,23 @@ android { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.debug + + ndk { + if (!project.hasProperty('target-platform')) { + abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64' + } else { + def platforms = project.property('target-platform').split(',') + def platformMap = [ + 'android-arm' : 'armeabi-v7a', + 'android-arm64': 'arm64-v8a', + 'android-x86' : 'x86', + 'android-x64' : 'x86_64', + ] + abiFilters = platforms.stream().map({ e -> + platformMap.containsKey(e) ? platformMap[e] : e + }).toArray() + } + } } } } diff --git a/example/lib/main.dart b/example/lib/main.dart index beb24be..30635d5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,4 @@ -/// -/// Copyright (C) 2018 Jason C.H +/// Copyright (C) 2018-2020 Jason C.H /// /// This library is free software; you can redistribute it and/or /// modify it under the terms of the GNU Lesser General Public @@ -10,8 +9,6 @@ /// but WITHOUT ANY WARRANTY; without even the implied warranty of /// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU /// Lesser General Public License for more details. -/// - import 'package:flutter/material.dart'; import 'package:flutter_vpn/flutter_vpn.dart'; @@ -28,7 +25,7 @@ class _MyAppState extends State { final _passwordController = TextEditingController(); var state = FlutterVpnState.disconnected; - var charonState = CharonErrorState.NO_ERROR; + CharonErrorState? charonState = CharonErrorState.NO_ERROR; @override void initState() { @@ -62,7 +59,7 @@ class _MyAppState extends State { obscureText: true, decoration: InputDecoration(icon: Icon(Icons.lock_outline)), ), - RaisedButton( + ElevatedButton( child: Text('Connect'), onPressed: () => FlutterVpn.simpleConnect( _addressController.text, @@ -70,22 +67,24 @@ class _MyAppState extends State { _passwordController.text, ), ), - RaisedButton( + ElevatedButton( child: Text('Disconnect'), onPressed: () => FlutterVpn.disconnect(), ), - RaisedButton( - child: Text('Update State'), - onPressed: () async { - var newState = await FlutterVpn.currentState; - setState(() => state = newState); - }), - RaisedButton( - child: Text('Update Charon State'), - onPressed: () async { - var newState = await FlutterVpn.charonErrorState; - setState(() => charonState = newState); - }), + ElevatedButton( + child: Text('Update State'), + onPressed: () async { + var newState = await FlutterVpn.currentState; + setState(() => state = newState); + }, + ), + ElevatedButton( + child: Text('Update Charon State'), + onPressed: () async { + var newState = await FlutterVpn.charonErrorState; + setState(() => charonState = newState); + }, + ), ], ), ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 59ec3b4..afcc1e3 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -6,7 +6,7 @@ description: Demonstrates how to use the flutter_vpn plugin. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: flutter: diff --git a/lib/flutter_vpn.dart b/lib/flutter_vpn.dart index 340dac0..936eb08 100644 --- a/lib/flutter_vpn.dart +++ b/lib/flutter_vpn.dart @@ -1,5 +1,4 @@ -/// -/// Copyright (C) 2018 Jason C.H +/// Copyright (C) 2018-2020 Jason C.H /// /// This library is free software; you can redistribute it and/or /// modify it under the terms of the GNU Lesser General Public @@ -10,8 +9,6 @@ /// but WITHOUT ANY WARRANTY; without even the implied warranty of /// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU /// Lesser General Public License for more details. -/// - import 'dart:io'; import 'package:flutter/services.dart'; @@ -25,7 +22,7 @@ enum FlutterVpnState { connecting, connected, disconnecting, - genericError + genericError, } /// The error state from `VpnStateService`. @@ -52,16 +49,23 @@ class FlutterVpn { .map((e) => FlutterVpnState.values[e]); /// Get current state. - static Future get currentState async => FlutterVpnState - .values[await _channel.invokeMethod('getCurrentState')]; + static Future get currentState async { + var state = await _channel.invokeMethod('getCurrentState'); + assert(state != null, 'Received a null state from `getCurrentState` call.'); + return FlutterVpnState.values[state!]; + } /// Get current error state from `VpnStateService`. (Android only) /// When [FlutterVpnState.genericError] is received, details of error can be - /// inspected by [CharonErrorState]. - static Future get charonErrorState async { - if (!Platform.isAndroid) throw Exception('Unsupport Platform'); + /// inspected by [CharonErrorState]. Returns [null] on non-android platform. + static Future get charonErrorState async { + if (!Platform.isAndroid) return null; var state = await _channel.invokeMethod('getCharonErrorState'); - return CharonErrorState.values[state]; + assert( + state != null, + 'Received a null state from `getCharonErrorState` call.', + ); + return CharonErrorState.values[state!]; } /// Prepare for vpn connection. (Android only) @@ -70,20 +74,20 @@ class FlutterVpn { /// When your connection was interrupted by another VPN connection, /// you should prepare again before reconnect. /// - /// Do nothing in iOS. + /// Does nothing on iOS. static Future prepare() async { if (!Platform.isAndroid) return true; - return await _channel.invokeMethod('prepare'); + return (await _channel.invokeMethod('prepare'))!; } /// Check if vpn connection has been prepared. (Android only) static Future get prepared async { if (!Platform.isAndroid) return true; - return await _channel.invokeMethod('prepared'); + return (await _channel.invokeMethod('prepared'))!; } /// Disconnect and stop VPN service. - static Future disconnect() async { + static Future disconnect() async { await _channel.invokeMethod('disconnect'); } @@ -92,14 +96,21 @@ class FlutterVpn { /// Use given credentials to connect VPN (ikev2-eap). /// This will create a background VPN service. /// MTU is only available on android. - static Future simpleConnect( - String address, String username, String password, - {int mtu = 1400}) async { + static Future simpleConnect( + String server, + String username, + String password, { + String? name, + int? port, + int? mtu, + }) async { await _channel.invokeMethod('connect', { - 'address': address, + 'name': name ?? server, + 'server': server, 'username': username, 'password': password, - 'mtu': mtu.toString() + if (port != null) 'port': port, + if (mtu != null) 'mtu': mtu, }); } } diff --git a/pubspec.yaml b/pubspec.yaml index 404074a..0514b1e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,13 +1,13 @@ name: flutter_vpn description: Plugin for developers to access VPN service in their flutter app. -version: 0.8.1 +version: 0.10.0 authors: - Jason C.H - Jerry Wang homepage: https://github.com/X-dea/Flutter_VPN environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" flutter: ">=1.20.0" dependencies: