Add android command client support
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,9 @@ interface PlatformInterfaceWrapper : PlatformInterface {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun clearDNSCache() {
|
||||
}
|
||||
|
||||
private class InterfaceArray(private val iterator: Enumeration<NetworkInterface>) :
|
||||
NetworkInterfaceIterator {
|
||||
|
||||
|
||||
@@ -142,6 +142,10 @@ class VPNService : VpnService(), PlatformInterfaceWrapper {
|
||||
return pfd.fd
|
||||
}
|
||||
|
||||
override fun closeTun() {
|
||||
service.onRevoke()
|
||||
}
|
||||
|
||||
override fun writeLog(message: String) = service.writeLog(message)
|
||||
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user