diff --git a/android/app/build.gradle b/android/app/build.gradle index 8fdc80c8..2b5f1845 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -101,6 +101,7 @@ flutter { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar','*.aar']) + implementation 'com.google.code.gson:gson:2.10.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.appcompat:appcompat:1.6.1' diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/GroupsChannel.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/GroupsChannel.kt new file mode 100644 index 00000000..0729a077 --- /dev/null +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/GroupsChannel.kt @@ -0,0 +1,86 @@ +package com.hiddify.hiddify + +import android.util.Log +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import com.hiddify.hiddify.utils.CommandClient +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.EventChannel +import io.nekohasekai.libbox.OutboundGroup +import io.nekohasekai.libbox.OutboundGroupItem +import kotlinx.coroutines.CoroutineScope + +class GroupsChannel(private val scope: CoroutineScope) : FlutterPlugin, CommandClient.Handler { + companion object { + const val TAG = "A/GroupsChannel" + const val GROUP_CHANNEL = "com.hiddify.app/groups" + val gson = Gson() + } + + private val commandClient = + CommandClient(scope, CommandClient.ConnectionType.Groups, this) + + private lateinit var groupsChannel: EventChannel + + private var groupsEvent: EventChannel.EventSink? = null + + override fun updateGroups(groups: List) { + MainActivity.instance.runOnUiThread { + val kGroups = groups.map { group -> KOutboundGroup.fromOutbound(group) } + Log.d(TAG, kGroups.toString()) + groupsEvent?.success(gson.toJson(kGroups)) + } + } + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + groupsChannel = EventChannel( + flutterPluginBinding.binaryMessenger, + GROUP_CHANNEL + ) + + groupsChannel.setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + groupsEvent = events + Log.d(TAG, "connecting groups command client") + commandClient.connect() + } + + override fun onCancel(arguments: Any?) { + groupsEvent = null + Log.d(TAG, "disconnecting groups command client") + commandClient.disconnect() + } + }) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + groupsEvent = null + commandClient.disconnect() + } + + data class KOutboundGroup( + val tag: String, + val type: String, + val selected: String, + val items: List + ) { + companion object { + fun fromOutbound(group: OutboundGroup): KOutboundGroup { + val outboundItems = group.items + val items = mutableListOf() + while (outboundItems.hasNext()) { + items.add(KOutboundGroupItem(outboundItems.next())) + } + return KOutboundGroup(group.tag, group.type, group.selected, items) + } + } + } + + data class KOutboundGroupItem( + val tag: String, + val type: String, + @SerializedName("url-test-delay") val urlTestDelay: Int, + ) { + constructor(item: OutboundGroupItem) : this(item.tag, item.type, item.urlTestDelay) + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt index 999cfaf2..ec3288f2 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/MainActivity.kt @@ -42,6 +42,7 @@ class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback { flutterEngine.plugins.add(MethodHandler()) flutterEngine.plugins.add(EventHandler()) flutterEngine.plugins.add(LogHandler()) + flutterEngine.plugins.add(GroupsChannel(lifecycleScope)) } fun reconnect() { diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt index cec43eb8..667d1349 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/MethodHandler.kt @@ -6,6 +6,9 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.StandardMethodCodec +import io.nekohasekai.libbox.Libbox +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler { private lateinit var channel: MethodChannel @@ -18,6 +21,8 @@ class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler { SetActiveConfigPath("set_active_config_path"), Start("start"), Stop("stop"), + SelectOutbound("select_outbound"), + UrlTest("url_test"), } } @@ -39,10 +44,14 @@ class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { Trigger.ParseConfig.method -> { - val args = call.arguments as Map<*, *> - val path = args["path"] as String? ?: "" - val msg = BoxService.parseConfig(path) - result.success(msg) + GlobalScope.launch { + result.runCatching { + val args = call.arguments as Map<*, *> + val path = args["path"] as String? ?: "" + val msg = BoxService.parseConfig(path) + success(msg) + } + } } Trigger.SetActiveConfigPath.method -> { @@ -61,6 +70,33 @@ class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler { result.success(true) } + Trigger.SelectOutbound.method -> { + GlobalScope.launch { + result.runCatching { + val args = call.arguments as Map<*, *> + Libbox.newStandaloneCommandClient() + .selectOutbound( + args["groupTag"] as String, + args["outboundTag"] as String + ) + success(true) + } + } + } + + Trigger.UrlTest.method -> { + GlobalScope.launch { + result.runCatching { + val args = call.arguments as Map<*, *> + Libbox.newStandaloneCommandClient() + .urlTest( + args["groupTag"] as String + ) + success(true) + } + } + } + else -> result.notImplemented() } } diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt index 3b7114c4..9345bef7 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/PlatformInterfaceWrapper.kt @@ -98,6 +98,9 @@ interface PlatformInterfaceWrapper : PlatformInterface { return false } + override fun clearDNSCache() { + } + private class InterfaceArray(private val iterator: Enumeration) : NetworkInterfaceIterator { diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt index e1407dee..76b19b69 100644 --- a/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/bg/VPNService.kt @@ -142,6 +142,10 @@ class VPNService : VpnService(), PlatformInterfaceWrapper { return pfd.fd } + override fun closeTun() { + service.onRevoke() + } + override fun writeLog(message: String) = service.writeLog(message) } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/ktx/Wrappers.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/ktx/Wrappers.kt new file mode 100644 index 00000000..85fc0984 --- /dev/null +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/ktx/Wrappers.kt @@ -0,0 +1,11 @@ +package com.hiddify.hiddify.ktx + +import io.nekohasekai.libbox.StringIterator + +fun StringIterator.toList(): List { + return mutableListOf().apply { + while (hasNext()) { + add(next()) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/hiddify/hiddify/utils/CommandClient.kt b/android/app/src/main/kotlin/com/hiddify/hiddify/utils/CommandClient.kt new file mode 100644 index 00000000..022f856d --- /dev/null +++ b/android/app/src/main/kotlin/com/hiddify/hiddify/utils/CommandClient.kt @@ -0,0 +1,133 @@ +package com.hiddify.hiddify.utils + +import go.Seq +import io.nekohasekai.libbox.CommandClient +import io.nekohasekai.libbox.CommandClientHandler +import io.nekohasekai.libbox.CommandClientOptions +import io.nekohasekai.libbox.Libbox +import io.nekohasekai.libbox.OutboundGroup +import io.nekohasekai.libbox.OutboundGroupIterator +import io.nekohasekai.libbox.StatusMessage +import io.nekohasekai.libbox.StringIterator +import com.hiddify.hiddify.ktx.toList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch + +open class CommandClient( + private val scope: CoroutineScope, + private val connectionType: ConnectionType, + private val handler: Handler +) { + + enum class ConnectionType { + Status, Groups, Log, ClashMode + } + + interface Handler { + + fun onConnected() {} + fun onDisconnected() {} + fun updateStatus(status: StatusMessage) {} + fun updateGroups(groups: List) {} + fun appendLog(message: String) {} + fun initializeClashMode(modeList: List, currentMode: String) {} + fun updateClashMode(newMode: String) {} + + } + + + private var commandClient: CommandClient? = null + private val clientHandler = ClientHandler() + fun connect() { + disconnect() + val options = CommandClientOptions() + options.command = when (connectionType) { + ConnectionType.Status -> Libbox.CommandStatus + ConnectionType.Groups -> Libbox.CommandGroup + ConnectionType.Log -> Libbox.CommandLog + ConnectionType.ClashMode -> Libbox.CommandClashMode + } + options.statusInterval = 2 * 1000 * 1000 * 1000 + val commandClient = CommandClient(clientHandler, options) + scope.launch(Dispatchers.IO) { + for (i in 1..10) { + delay(100 + i.toLong() * 50) + try { + commandClient.connect() + } catch (ignored: Exception) { + continue + } + if (!isActive) { + runCatching { + commandClient.disconnect() + } + return@launch + } + this@CommandClient.commandClient = commandClient + return@launch + } + runCatching { + commandClient.disconnect() + } + } + } + + fun disconnect() { + commandClient?.apply { + runCatching { + disconnect() + } + Seq.destroyRef(refnum) + } + commandClient = null + } + + private inner class ClientHandler : CommandClientHandler { + + override fun connected() { + handler.onConnected() + } + + override fun disconnected(message: String?) { + handler.onDisconnected() + } + + override fun writeGroups(message: OutboundGroupIterator?) { + if (message == null) { + return + } + val groups = mutableListOf() + while (message.hasNext()) { + groups.add(message.next()) + } + handler.updateGroups(groups) + } + + override fun writeLog(message: String?) { + if (message == null) { + return + } + handler.appendLog(message) + } + + override fun writeStatus(message: StatusMessage?) { + if (message == null) { + return + } + handler.updateStatus(message) + } + + override fun initializeClashMode(modeList: StringIterator, currentMode: String) { + handler.initializeClashMode(modeList.toList(), currentMode) + } + + override fun updateClashMode(newMode: String) { + handler.updateClashMode(newMode) + } + + } + +} \ No newline at end of file