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:
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/.tauri
|
||||
@@ -0,0 +1,45 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.plugin.vpnservice"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
targetSdk = 34
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.0")
|
||||
implementation("com.google.android.material:material:1.7.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
implementation(project(":tauri-android"))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,2 @@
|
||||
include ':tauri-android'
|
||||
project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user