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 d4a10e4..7b09661 100644 --- a/android/src/main/java/org/strongswan/android/logic/CharonVpnService.java +++ b/android/src/main/java/org/strongswan/android/logic/CharonVpnService.java @@ -56,6 +56,9 @@ import java.util.SortedSet; import java.util.UUID; +import io.flutter.plugin.common.EventChannel; +import io.xdea.fluttervpn.VPNStateHandler; + public class CharonVpnService extends VpnService implements Runnable { private static final String NOTIFICATION_CHANNEL = "org.strongswan.android.CharonVpnService.VPN_STATE_NOTIFICATION"; @@ -110,12 +113,12 @@ public int onStartCommand(Intent intent, int flags, int startId) { Bundle bundle = intent.getExtras(); VpnProfile profile = new VpnProfile(); profile.setId(1); - profile.setUUID(UUID.fromString(bundle.getString("_uuid"))); - profile.setName(bundle.getString("PROFILE_NAME")); - profile.setGateway(bundle.getString("PROFILE_NAME")); - profile.setVpnType(VpnType.fromIdentifier("ikev2-eap")); + profile.setUUID(UUID.randomUUID()); + profile.setName(bundle.getString("address")); + profile.setGateway(bundle.getString("address")); profile.setUsername(bundle.getString("username")); profile.setPassword(bundle.getString("password")); + profile.setVpnType(VpnType.fromIdentifier("ikev2-eap")); profile.setSelectedAppsHandling(0); profile.setFlags(0); setNextProfile(profile); @@ -226,6 +229,11 @@ public void run() { public void updateStatus(int status) { NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); manager.notify(1, buildNotification()); + + // Update state through event channel. + EventChannel.EventSink sink = VPNStateHandler.Companion.getEventHandler(); + if(sink != null) + sink.success(status); } /** diff --git a/android/src/main/kotlin/io/xdea/fluttervpn/FlutterVpnPlugin.kt b/android/src/main/kotlin/io/xdea/fluttervpn/FlutterVpnPlugin.kt index 9767454..056c2a2 100644 --- a/android/src/main/kotlin/io/xdea/fluttervpn/FlutterVpnPlugin.kt +++ b/android/src/main/kotlin/io/xdea/fluttervpn/FlutterVpnPlugin.kt @@ -1,3 +1,17 @@ +/** + * Copyright (C) 2018 Jason C.H + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + package io.xdea.fluttervpn import android.app.Activity.RESULT_OK @@ -5,6 +19,7 @@ import android.content.Intent import android.net.VpnService import android.os.Bundle import android.support.v4.content.ContextCompat +import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -22,16 +37,20 @@ class FlutterVpnPlugin(private val registrar: Registrar) : MethodCallHandler { companion object { @JvmStatic fun registerWith(registrar: Registrar) { - // Register method channel + // Register method channel. val channel = MethodChannel(registrar.messenger(), "flutter_vpn") channel.setMethodCallHandler(FlutterVpnPlugin(registrar)) + + // Register event channel to handle state change. + val eventChannel = EventChannel(registrar.messenger(), "flutter_vpn_states") + eventChannel.setStreamHandler(VPNStateHandler()) } fun onPrepareResult(requestCode: Int, resultCode: Int, result: Result): Boolean { if (requestCode == 0 && resultCode == RESULT_OK) result.success(true) else - result.error("error", "Failed to prepare", false) + result.error("PrepareError", "Failed to prepare", false) return true } } @@ -43,15 +62,15 @@ class FlutterVpnPlugin(private val registrar: Registrar) : MethodCallHandler { val intent = VpnService.prepare(registrar.activeContext()) if (intent != null) { registrar.addActivityResultListener { req, res, _ -> onPrepareResult(req, res, result) } - registrar.activity().startActivityForResult(intent, 0) - } } "connect" -> { val intent = VpnService.prepare(registrar.activeContext()) - if (intent != null) - result.error("error", "Not prepared", false) + if (intent != null) { + result.error("PrepareError", "Not prepared", false) + return + } val map = call.arguments as HashMap val address = map["address"] val username = map["username"] @@ -69,15 +88,12 @@ class FlutterVpnPlugin(private val registrar: Registrar) : MethodCallHandler { private fun connect(address: String?, username: String?, password: String?) { val profileInfo = Bundle() - profileInfo.putString("_uuid", "be869700-4ad4-4215-8453-619a1472b384") + profileInfo.putString("address", address) profileInfo.putString("username", username) profileInfo.putString("password", password) - profileInfo.putBoolean("REQUIRES_PASSWORD", true) - profileInfo.putString("PROFILE_NAME", address) - /* we assume we have the necessary permission */ - val intent = Intent(registrar.activeContext(), CharonVpnService::class.java) - intent.putExtras(profileInfo) + val intent = Intent(registrar.activeContext(), CharonVpnService::class.java) + .putExtras(profileInfo) ContextCompat.startForegroundService(registrar.activeContext(), intent) } @@ -92,6 +108,4 @@ class FlutterVpnPlugin(private val registrar: Registrar) : MethodCallHandler { intent.action = CharonVpnService.DISCONNECT_ACTION registrar.activeContext().startService(intent) } - - } diff --git a/android/src/main/kotlin/io/xdea/fluttervpn/VPNStateHandler.kt b/android/src/main/kotlin/io/xdea/fluttervpn/VPNStateHandler.kt new file mode 100644 index 0000000..86a9158 --- /dev/null +++ b/android/src/main/kotlin/io/xdea/fluttervpn/VPNStateHandler.kt @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2018 Jason C.H + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +package io.xdea.fluttervpn + +import io.flutter.plugin.common.EventChannel + +/* + STATE_CHILD_SA_UP = 1; + STATE_CHILD_SA_DOWN = 2; + STATE_AUTH_ERROR = 3; + STATE_PEER_AUTH_ERROR = 4; + STATE_LOOKUP_ERROR = 5; + STATE_UNREACHABLE_ERROR = 6; + STATE_CERTIFICATE_UNAVAILABLE = 7; + STATE_GENERIC_ERROR = 8; + */ + +class VPNStateHandler : EventChannel.StreamHandler { + + companion object { + /** + * The charon VPN service will update state through the sink if not `null`. + */ + var eventHandler: EventChannel.EventSink? = null + } + + override fun onListen(p0: Any?, sink: EventChannel.EventSink?) { + eventHandler = sink + } + + override fun onCancel(p0: Any?) { + eventHandler = null + } +} \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 28bd68d..f66d6ed 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -27,9 +27,12 @@ class _MyAppState extends State { final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); + var state = FlutterVpnState.down; + @override void initState() { FlutterVpn.prepare(); + FlutterVpn.onStateChanged.listen((s) => setState(() => state = s)); super.initState(); } @@ -43,6 +46,7 @@ class _MyAppState extends State { body: ListView( padding: const EdgeInsets.all(15.0), children: [ + Text('Current State: $state'), TextFormField( controller: _addressController, decoration: InputDecoration(icon: Icon(Icons.map)), @@ -53,6 +57,7 @@ class _MyAppState extends State { ), TextFormField( controller: _passwordController, + obscureText: true, decoration: InputDecoration(icon: Icon(Icons.lock_outline)), ), RaisedButton( diff --git a/lib/flutter_vpn.dart b/lib/flutter_vpn.dart index 9618960..29cfab2 100644 --- a/lib/flutter_vpn.dart +++ b/lib/flutter_vpn.dart @@ -16,14 +16,52 @@ import 'dart:async'; import 'package:flutter/services.dart'; +const _channel = const MethodChannel('flutter_vpn'); +const _eventChannel = const EventChannel('flutter_vpn_states'); + +enum FlutterVpnState { + up, + down, + authError, + peerAuthError, + lookUpError, + unreachableError, + certificateUnavailable, + genericError +} + class FlutterVpn { - static const MethodChannel _channel = const MethodChannel('flutter_vpn'); + /// Receive state change from charon VPN service. + /// + /// Can only be listened once. + /// If more than one, only the last subscription will receive events. + static Stream get onStateChanged => + _eventChannel.receiveBroadcastStream().map((event) { + switch (event) { + case 1: + return FlutterVpnState.up; + case 2: + return FlutterVpnState.down; + case 3: + return FlutterVpnState.authError; + case 4: + return FlutterVpnState.peerAuthError; + case 5: + return FlutterVpnState.lookUpError; + case 6: + return FlutterVpnState.unreachableError; + case 7: + return FlutterVpnState.certificateUnavailable; + case 8: + return FlutterVpnState.genericError; + } + }); /// Prepare for vpn connection. /// /// For first connection it will show a dialog to ask for permission. /// When your connection was interrupted by another VPN connection, - /// you should prepare again before reconnection. + /// you should prepare again before reconnect. static Future prepare() async { return await _channel.invokeMethod('prepare'); }