Android Support (#166)

1. Add vpnservice tauri plugin for android.
2. add workflow for android.
3. Easytier Core support android, allow set tun fd.
This commit is contained in:
Sijie.Sun
2024-07-15 00:03:55 +08:00
committed by GitHub
parent 4938e3ed2b
commit 858ade2eee
113 changed files with 3847 additions and 537 deletions
@@ -0,0 +1,24 @@
package com.plugin.vpnservice
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.plugin.vpnservice", appContext.packageName)
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
@@ -0,0 +1,10 @@
package com.plugin.vpnservice
import android.util.Log
class Example {
fun pong(value: String): String {
Log.i("Pong", value)
return value
}
}
@@ -0,0 +1,115 @@
package com.plugin.vpnservice
import android.content.Intent
import android.net.VpnService
import android.net.IpPrefix
import android.os.Build
import android.os.ParcelFileDescriptor
import android.os.Bundle
import java.net.InetAddress
import java.util.Arrays
import app.tauri.plugin.JSObject
fun stringToIpPrefix(ipPrefixString: String): IpPrefix {
val parts = ipPrefixString.split("/")
if (parts.size != 2) throw IllegalArgumentException("Invalid IP prefix string")
val address = InetAddress.getByName(parts[0])
val prefixLength = parts[1].toInt()
return IpPrefix(address, prefixLength)
}
class TauriVpnService : VpnService() {
companion object {
@JvmField var triggerCallback: (String, JSObject) -> Unit = { _, _ -> }
@JvmField var self: TauriVpnService? = null
const val IPV4_ADDR = "IPV4_ADDR"
const val ROUTES = "ROUTES"
const val DNS = "DNS"
const val DISALLOWED_APPLICATIONS = "DISALLOWED_APPLICATIONS"
const val MTU = "MTU"
}
private lateinit var vpnInterface: ParcelFileDescriptor
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
println("vpn on start command ${intent?.getExtras()} $intent")
var args = intent?.getExtras()
vpnInterface = createVpnInterface(args)
println("vpn created ${vpnInterface.fd}")
var event_data = JSObject()
event_data.put("fd", vpnInterface.fd)
triggerCallback("vpn_service_start", event_data)
return START_STICKY
}
override fun onCreate() {
super.onCreate()
self = this
println("vpn on create")
}
override fun onDestroy() {
println("vpn on destroy")
self = null
super.onDestroy()
disconnect()
}
override fun onRevoke() {
println("vpn on revoke")
self = null
super.onRevoke()
disconnect()
}
private fun disconnect() {
triggerCallback("vpn_service_stop", JSObject())
vpnInterface.close()
}
private fun createVpnInterface(args: Bundle?): ParcelFileDescriptor {
var builder = Builder()
.setSession("TauriVpnService")
.setBlocking(false)
var mtu = args?.getInt(MTU) ?: 1500
var ipv4Addr = args?.getString(IPV4_ADDR) ?: "10.126.126.1/24"
var dns = args?.getString(DNS) ?: "114.114.114.114"
var routes = args?.getStringArray(ROUTES) ?: emptyArray()
var disallowedApplications = args?.getStringArray(DISALLOWED_APPLICATIONS) ?: emptyArray()
println("vpn create vpn interface. mtu: $mtu, ipv4Addr: $ipv4Addr, dns:" +
"$dns, routes: ${java.util.Arrays.toString(routes)}," +
"disallowedApplications: ${java.util.Arrays.toString(disallowedApplications)}")
val ipParts = ipv4Addr.split("/")
if (ipParts.size != 2) throw IllegalArgumentException("Invalid IP addr string")
builder.addAddress(ipParts[0], ipParts[1].toInt())
builder.setMtu(mtu)
builder.addDnsServer(dns)
for (route in routes) {
builder.addRoute(stringToIpPrefix(route))
}
for (app in disallowedApplications) {
builder.addDisallowedApplication(app)
}
return builder.also {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
it.setMetered(false)
}
}
.establish()
?: throw IllegalStateException("Failed to init VpnService")
}
}
@@ -0,0 +1,93 @@
package com.plugin.vpnservice
import android.app.Activity
import android.content.Intent
import android.net.VpnService
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.Invoke
import app.tauri.plugin.JSObject
import app.tauri.plugin.Plugin
import android.webkit.WebView
@InvokeArg
class PingArgs {
var value: String? = null
}
@InvokeArg
class StartVpnArgs {
var ipv4Addr: String? = null
var routes: Array<String> = emptyArray()
var dns: String? = null
var disallowedApplications: Array<String> = emptyArray()
var mtu: Int? = null
}
@TauriPlugin
class VpnServicePlugin(private val activity: Activity) : Plugin(activity) {
private val implementation = Example()
override fun load(webView: WebView) {
println("load vpn service plugin")
TauriVpnService.triggerCallback = { event, data ->
println("vpn: triggerCallback $event $data")
trigger(event, data)
}
}
@Command
fun ping(invoke: Invoke) {
val args = invoke.parseArgs(PingArgs::class.java)
val ret = JSObject()
ret.put("value", implementation.pong(args.value ?: "default value :("))
invoke.resolve(ret)
}
@Command
fun prepareVpn(invoke: Invoke) {
println("prepare vpn in plugin")
val it = VpnService.prepare(activity)
var ret = JSObject()
if (it != null) {
activity.startActivityForResult(it, 0x0f)
ret.put("errorMsg", "again")
}
invoke.resolve(ret)
}
@Command
fun startVpn(invoke: Invoke) {
val args = invoke.parseArgs(StartVpnArgs::class.java)
println("start vpn in plugin, args: $args")
TauriVpnService.self?.onRevoke()
val it = VpnService.prepare(activity)
var ret = JSObject()
if (it != null) {
ret.put("errorMsg", "need_prepare")
} else {
var intent = Intent(activity, TauriVpnService::class.java)
intent.putExtra(TauriVpnService.IPV4_ADDR, args.ipv4Addr)
intent.putExtra(TauriVpnService.ROUTES, args.routes)
intent.putExtra(TauriVpnService.DNS, args.dns)
intent.putExtra(TauriVpnService.DISALLOWED_APPLICATIONS, args.disallowedApplications)
intent.putExtra(TauriVpnService.MTU, args.mtu)
activity.startService(intent)
}
invoke.resolve(ret)
}
@Command
fun stopVpn(invoke: Invoke) {
println("stop vpn in plugin")
TauriVpnService.self?.onRevoke()
activity.stopService(Intent(activity, TauriVpnService::class.java))
println("stop vpn in plugin end")
invoke.resolve(JSObject())
}
}
@@ -0,0 +1,17 @@
package com.plugin.vpnservice
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}