Add android command client support
This commit is contained in:
@@ -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'
|
||||||
|
|||||||
@@ -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(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() {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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