Add android command client support

This commit is contained in:
problematicconsumer
2023-08-29 19:16:17 +03:30
parent 1ca389082f
commit 375cb8a945
8 changed files with 279 additions and 4 deletions

View File

@@ -101,6 +101,7 @@ flutter {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar','*.aar']) 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 "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'

View File

@@ -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<OutboundGroup>) {
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<KOutboundGroupItem>
) {
companion object {
fun fromOutbound(group: OutboundGroup): KOutboundGroup {
val outboundItems = group.items
val items = mutableListOf<KOutboundGroupItem>()
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)
}
}

View File

@@ -42,6 +42,7 @@ class MainActivity : FlutterFragmentActivity(), ServiceConnection.Callback {
flutterEngine.plugins.add(MethodHandler()) flutterEngine.plugins.add(MethodHandler())
flutterEngine.plugins.add(EventHandler()) flutterEngine.plugins.add(EventHandler())
flutterEngine.plugins.add(LogHandler()) flutterEngine.plugins.add(LogHandler())
flutterEngine.plugins.add(GroupsChannel(lifecycleScope))
} }
fun reconnect() { fun reconnect() {

View File

@@ -6,6 +6,9 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.StandardMethodCodec import io.flutter.plugin.common.StandardMethodCodec
import io.nekohasekai.libbox.Libbox
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler { class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var channel: MethodChannel private lateinit var channel: MethodChannel
@@ -18,6 +21,8 @@ class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler {
SetActiveConfigPath("set_active_config_path"), SetActiveConfigPath("set_active_config_path"),
Start("start"), Start("start"),
Stop("stop"), 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) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
Trigger.ParseConfig.method -> { Trigger.ParseConfig.method -> {
val args = call.arguments as Map<*, *> GlobalScope.launch {
val path = args["path"] as String? ?: "" result.runCatching {
val msg = BoxService.parseConfig(path) val args = call.arguments as Map<*, *>
result.success(msg) val path = args["path"] as String? ?: ""
val msg = BoxService.parseConfig(path)
success(msg)
}
}
} }
Trigger.SetActiveConfigPath.method -> { Trigger.SetActiveConfigPath.method -> {
@@ -61,6 +70,33 @@ class MethodHandler : FlutterPlugin, MethodChannel.MethodCallHandler {
result.success(true) 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() else -> result.notImplemented()
} }
} }

View File

@@ -98,6 +98,9 @@ interface PlatformInterfaceWrapper : PlatformInterface {
return false return false
} }
override fun clearDNSCache() {
}
private class InterfaceArray(private val iterator: Enumeration<NetworkInterface>) : private class InterfaceArray(private val iterator: Enumeration<NetworkInterface>) :
NetworkInterfaceIterator { NetworkInterfaceIterator {

View File

@@ -142,6 +142,10 @@ class VPNService : VpnService(), PlatformInterfaceWrapper {
return pfd.fd return pfd.fd
} }
override fun closeTun() {
service.onRevoke()
}
override fun writeLog(message: String) = service.writeLog(message) override fun writeLog(message: String) = service.writeLog(message)
} }

View File

@@ -0,0 +1,11 @@
package com.hiddify.hiddify.ktx
import io.nekohasekai.libbox.StringIterator
fun StringIterator.toList(): List<String> {
return mutableListOf<String>().apply {
while (hasNext()) {
add(next())
}
}
}

View File

@@ -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<OutboundGroup>) {}
fun appendLog(message: String) {}
fun initializeClashMode(modeList: List<String>, 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<OutboundGroup>()
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)
}
}
}