shorter version, but work [AI FULL]
This commit is contained in:
@@ -1,15 +1,11 @@
|
||||
package app.netrunner.vpn
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.VpnService
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.Parcelable
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.result.ActivityResult
|
||||
@@ -41,513 +37,199 @@ data class TunnelConfig @JsonCreator constructor(
|
||||
@JsonProperty("mtu") val mtu: Int
|
||||
) : Parcelable
|
||||
|
||||
// ------------------------------------------------
|
||||
// VPN SERVICE
|
||||
// ------------------------------------------------
|
||||
|
||||
class TunVpnService : VpnService() {
|
||||
|
||||
private var activeSession: Session? = null
|
||||
private var vpnInterface: ParcelFileDescriptor? = null
|
||||
|
||||
companion object {
|
||||
const val ACTION_STOP_VPN = "app.netrunner.vpn.STOP_FROM_NOTIFICATION"
|
||||
@Volatile
|
||||
var isServiceRunning = false
|
||||
@Volatile var isServiceRunning = false
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
isServiceRunning = true
|
||||
Log.d(TAG, "TunVpnService created")
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
||||
Log.d(TAG, "onStartCommand called action=${intent?.action}")
|
||||
startForegroundNotification()
|
||||
when (intent?.action) {
|
||||
|
||||
"START_VPN" -> {
|
||||
|
||||
val ip = intent.getStringExtra("ip") ?: ""
|
||||
val port = intent.getIntExtra("port", 0)
|
||||
|
||||
Log.d(TAG, "START_VPN received ip=$ip port=$port")
|
||||
|
||||
val config = if (android.os.Build.VERSION.SDK_INT >= 33) {
|
||||
intent.getParcelableExtra("config", TunnelConfig::class.java)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra("config")
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
Log.e(TAG, "TunnelConfig is NULL")
|
||||
} else {
|
||||
Log.d(TAG, "Config received: $config")
|
||||
startVpn(config, ip, port)
|
||||
}
|
||||
}
|
||||
|
||||
"STOP_VPN", ACTION_STOP_VPN -> {
|
||||
Log.d(TAG, "STOP_VPN received")
|
||||
stopServiceProperly()
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Unknown action ${intent?.action}")
|
||||
val config = if (Build.VERSION.SDK_INT >= 33) intent.getParcelableExtra("config", TunnelConfig::class.java)
|
||||
else @Suppress("DEPRECATION") intent.getParcelableExtra("config")
|
||||
config?.let { startVpn(it, ip, port) }
|
||||
}
|
||||
"STOP_VPN", ACTION_STOP_VPN -> stopServiceProperly()
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun sendStatusBroadcast(status: String) {
|
||||
val intent = Intent("com.netrunner.vpn.STATUS_UPDATE").apply {
|
||||
putExtra("status", status)
|
||||
// Указываем пакет нашего приложения, чтобы интент не улетел в другие аппки
|
||||
// и гарантированно дошел до нашего VpnPlugin
|
||||
setPackage(packageName)
|
||||
private fun startVpn(config: TunnelConfig, ip: String, port: Int) {
|
||||
val builder = Builder().addAddress(config.address, config.prefix)
|
||||
.addDnsServer(config.dns).addRoute("0.0.0.0", 0).setMtu(config.mtu)
|
||||
|
||||
try { builder.addDisallowedApplication(packageName) } catch (e: Exception) { Log.e(TAG, "Exclude fail") }
|
||||
|
||||
vpnInterface = builder.establish() ?: return
|
||||
val fd = vpnInterface!!.detachFd()
|
||||
|
||||
try {
|
||||
activeSession = SessionManager().startMobile("$ip:$port", fd, cacheDir.absolutePath)
|
||||
triggerVibration()
|
||||
sendStatusBroadcast("connected")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Rust fail", e)
|
||||
stopServiceProperly()
|
||||
}
|
||||
sendBroadcast(intent)
|
||||
Log.d("VPN_DEBUG", ">>> [SERVICE] Broadcast sent: $status")
|
||||
}
|
||||
|
||||
private fun stopServiceProperly() {
|
||||
stopVpn()
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
|
||||
stopForeground(android.app.Service.STOP_FOREGROUND_REMOVE)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(true)
|
||||
}
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun startVpn(config: TunnelConfig, ip: String, port: Int) {
|
||||
Log.d(TAG, "startVpn called")
|
||||
|
||||
val builder = Builder()
|
||||
.addAddress(config.address, config.prefix)
|
||||
.addDnsServer(config.dns)
|
||||
.addRoute("0.0.0.0", 0)
|
||||
.setMtu(config.mtu)
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
try {
|
||||
// Исключаем само приложение из проксирования, чтобы не было петли
|
||||
builder.addDisallowedApplication(packageName)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Can't exclude app: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
vpnInterface = builder.establish()
|
||||
|
||||
if (vpnInterface == null) {
|
||||
Log.e(TAG, "builder.establish() returned null")
|
||||
return
|
||||
}
|
||||
|
||||
val fd = vpnInterface?.detachFd() ?: run {
|
||||
Log.e(TAG, "detachFd failed")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val manager = SessionManager()
|
||||
|
||||
val cachePath = cacheDir.absolutePath
|
||||
|
||||
Log.d(TAG, "Starting Rust Session with cachePath: $cachePath")
|
||||
|
||||
activeSession = manager.startMobile("$ip:$port", fd, cachePath)
|
||||
triggerVibration()
|
||||
sendStatusBroadcast("connected")
|
||||
Log.d(TAG, "Session started successfully")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Rust manager.startMobile failed", e)
|
||||
sendStatusBroadcast("idle")
|
||||
stopVpn()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopVpn() {
|
||||
|
||||
Log.d(TAG, "stopVpn called")
|
||||
triggerVibration()
|
||||
sendStatusBroadcast("idle")
|
||||
activeSession?.stop()
|
||||
activeSession = null
|
||||
|
||||
try {
|
||||
vpnInterface?.close()
|
||||
Log.d(TAG, "vpnInterface closed")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error closing interface", e)
|
||||
}
|
||||
|
||||
try { vpnInterface?.close() } catch (e: Exception) { }
|
||||
vpnInterface = null
|
||||
isServiceRunning = false
|
||||
sendStatusBroadcast("idle")
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun sendStatusBroadcast(status: String) {
|
||||
sendBroadcast(Intent("com.netrunner.vpn.STATUS_UPDATE").apply {
|
||||
putExtra("status", status)
|
||||
setPackage(packageName)
|
||||
})
|
||||
}
|
||||
|
||||
private fun startForegroundNotification() {
|
||||
val channelId = "vpn_service_channel"
|
||||
val channelName = "Netrunner VPN Service"
|
||||
val notificationManager = getSystemService(android.app.NotificationManager::class.java)
|
||||
|
||||
val stopIntent = Intent(this, TunVpnService::class.java).apply {
|
||||
action = ACTION_STOP_VPN
|
||||
val channelId = "vpn_channel"
|
||||
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
nm.createNotificationChannel(NotificationChannel(channelId, "VPN Service", NotificationManager.IMPORTANCE_LOW))
|
||||
}
|
||||
|
||||
val stopPendingIntent = android.app.PendingIntent.getService(
|
||||
this, 1, stopIntent,
|
||||
android.app.PendingIntent.FLAG_IMMUTABLE or android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
val stopPI = PendingIntent.getService(this, 1, Intent(this, TunVpnService::class.java).apply { action = ACTION_STOP_VPN },
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val contentPI = PendingIntent.getActivity(this, 0, packageManager.getLaunchIntentForPackage(packageName), PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
val channel = android.app.NotificationChannel(
|
||||
channelId, channelName, android.app.NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
description = "Показывает статус работы VPN"
|
||||
setShowBadge(false)
|
||||
}
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
// Создаем Intent, чтобы при клике на уведомление открывалось приложение
|
||||
val contentPendingIntent = android.app.PendingIntent.getActivity(
|
||||
this, 0,
|
||||
packageManager.getLaunchIntentForPackage(packageName),
|
||||
android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val stopAction = android.app.Notification.Action.Builder(
|
||||
android.R.drawable.ic_menu_close_clear_cancel, // Иконка
|
||||
"Отключить", // Текст
|
||||
stopPendingIntent // Действие
|
||||
).build()
|
||||
|
||||
val notification = android.app.Notification.Builder(this, channelId)
|
||||
.setContentTitle("Netrunner VPN")
|
||||
.setContentText("Защита активна")
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_info) // Стандартная системная иконка
|
||||
.setContentIntent(contentPendingIntent)
|
||||
.setOngoing(true)
|
||||
.addAction(stopAction)
|
||||
.setCategory(android.app.Notification.CATEGORY_SERVICE)
|
||||
val notification = (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) Notification.Builder(this, channelId) else Notification.Builder(this))
|
||||
.setContentTitle("Netrunner VPN").setContentText("Защита активна")
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
||||
.setContentIntent(contentPI).setOngoing(true)
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Отключить", stopPI)
|
||||
.build()
|
||||
|
||||
try {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(1, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else {
|
||||
startForeground(1, notification)
|
||||
}
|
||||
Log.d(TAG, "Foreground notification started successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to start foreground: ${e.message}")
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(1, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
||||
} else startForeground(1, notification)
|
||||
}
|
||||
|
||||
private fun triggerVibration() {
|
||||
val vibrator = getSystemService(android.content.Context.VIBRATOR_SERVICE) as android.os.Vibrator
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
vibrator.vibrate(android.os.VibrationEffect.createOneShot(500, android.os.VibrationEffect.EFFECT_HEAVY_CLICK))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrator.vibrate(500)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
|
||||
Log.d(TAG, "UI process killed (swiped), but VPN process should keep running.")
|
||||
val v = getSystemService(VIBRATOR_SERVICE) as Vibrator
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) v.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||
else @Suppress("DEPRECATION") v.vibrate(50)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "TunVpnService destroyed")
|
||||
sendStatusBroadcast("idle")
|
||||
isServiceRunning = false
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
// TAURI PLUGIN
|
||||
// ------------------------------------------------
|
||||
|
||||
@TauriPlugin
|
||||
class VpnPlugin(private val activity: Activity) : Plugin(activity) {
|
||||
|
||||
companion object {
|
||||
private var instance: VpnPlugin? = null
|
||||
|
||||
fun sendStatus(status: String) {
|
||||
val hasInstance = (instance != null)
|
||||
Log.d(TAG, ">>> [PLUGIN COMPANION] sendStatus called: $status | Instance exists: $hasInstance")
|
||||
if (hasInstance) {
|
||||
instance?.updateStatus(status)
|
||||
} else {
|
||||
Log.e(TAG, "CRITICAL: Cannot send status, VpnPlugin instance is NULL")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var pendingArgs: VpnArgs? = null
|
||||
|
||||
private val statusReceiver = object : android.content.BroadcastReceiver() {
|
||||
override fun onReceive(context: android.content.Context?, intent: android.content.Intent?) {
|
||||
val status = intent?.getStringExtra("status") ?: return
|
||||
Log.d(TAG, ">>> [RECEIVER] Got status from Service: $status")
|
||||
instance?.updateStatus(status)
|
||||
private val statusReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
intent?.getStringExtra("status")?.let { updateStatus(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerStatusReceiver() {
|
||||
val filter = IntentFilter("com.netrunner.vpn.STATUS_UPDATE")
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
activity.registerReceiver(statusReceiver, filter, android.content.Context.RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
activity.registerReceiver(statusReceiver, filter)
|
||||
}
|
||||
Log.d(TAG, "Status receiver registered successfully")
|
||||
}
|
||||
|
||||
override fun load(webView: WebView) {
|
||||
super.load(webView)
|
||||
instance = this
|
||||
Log.d(TAG, "VpnPlugin loaded with WebView and instance assigned")
|
||||
try { System.loadLibrary("netrunner_client") } catch (e: Exception) { Log.e(TAG, "Lib fail") }
|
||||
|
||||
try {
|
||||
System.loadLibrary("netrunner_client")
|
||||
Log.d(TAG, "Rust library loaded")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load Rust library: ${e.message}")
|
||||
val filter = IntentFilter("com.netrunner.vpn.STATUS_UPDATE")
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
activity.registerReceiver(statusReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
||||
} else {
|
||||
activity.registerReceiver(statusReceiver, filter)
|
||||
}
|
||||
|
||||
requestNotificationPermission()
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
kotlinx.coroutines.delay(500)
|
||||
val currentStatus = getHardwareVpnStatus()
|
||||
updateStatus(currentStatus)
|
||||
Log.d(TAG, "Plugin reloaded. Current status: $currentStatus")
|
||||
}
|
||||
}
|
||||
|
||||
private fun isVpnServiceRunning(): Boolean {
|
||||
val manager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
||||
@Suppress("DEPRECATION")
|
||||
return manager.getRunningServices(Integer.MAX_VALUE).any {
|
||||
it.service.className == TunVpnService::class.java.name
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHardwareVpnStatus(): String {
|
||||
val hasTun = try {
|
||||
java.net.NetworkInterface.getNetworkInterfaces().asSequence().any {
|
||||
it.isUp && it.name.contains("tun")
|
||||
}
|
||||
} catch (e: Exception) { false }
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
updateBlocklistIfNeeded()
|
||||
}
|
||||
|
||||
registerStatusReceiver()
|
||||
// Если интерфейс поднят ИЛИ сервис числится в запущенных — значит мы connected
|
||||
val isRunning = hasTun || isVpnServiceRunning() || TunVpnService.isServiceRunning
|
||||
return if (isRunning) "connected" else "idle"
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun updateBlocklistIfNeeded() {
|
||||
try {
|
||||
val cachePath = activity.cacheDir.absolutePath
|
||||
val hostsFile = File(cachePath, "hosts_cache.txt")
|
||||
|
||||
val isFresh = hostsFile.exists() &&
|
||||
(System.currentTimeMillis() - hostsFile.lastModified() < 24 * 60 * 60 * 1000)
|
||||
|
||||
if (isFresh) {
|
||||
Log.d(TAG, "DNS: Blocklist is fresh, skipping download")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "DNS: Starting blocklist download via Kotlin...")
|
||||
|
||||
// Качаем
|
||||
val url = URL("https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts")
|
||||
val connection = url.openConnection()
|
||||
connection.connectTimeout = 15000
|
||||
connection.readTimeout = 15000
|
||||
|
||||
val bytes = connection.getInputStream().use { it.readBytes() }
|
||||
|
||||
// Сохраняем
|
||||
hostsFile.writeBytes(bytes)
|
||||
|
||||
Log.d(TAG, "DNS: Blocklist updated! Size: ${bytes.size} bytes")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "DNS: Failed to download blocklist: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestNotificationPermission() {
|
||||
if (android.os.Build.VERSION.SDK_INT >= 33) {
|
||||
val permission = "android.permission.POST_NOTIFICATIONS"
|
||||
if (androidx.core.content.ContextCompat.checkSelfPermission(activity, permission) !=
|
||||
android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
androidx.core.app.ActivityCompat.requestPermissions(activity, arrayOf(permission), 101)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class VpnArgs {
|
||||
var config: TunnelConfig? = null
|
||||
var ip: String = ""
|
||||
var port: Int = 0
|
||||
}
|
||||
|
||||
private var pendingArgs: VpnArgs? = null
|
||||
|
||||
@Command
|
||||
fun startVpn(invoke: Invoke) {
|
||||
|
||||
Log.d(TAG, "startVpn command invoked")
|
||||
|
||||
val args = invoke.parseArgs(VpnArgs::class.java)
|
||||
|
||||
Log.d(TAG, "Args parsed ip=${args.ip} port=${args.port}")
|
||||
|
||||
val config = args.config
|
||||
|
||||
if (config == null) {
|
||||
Log.e(TAG, "Config is null")
|
||||
invoke.reject("Config is null")
|
||||
return
|
||||
}
|
||||
|
||||
val permissionIntent = VpnService.prepare(activity)
|
||||
|
||||
if (permissionIntent != null) {
|
||||
|
||||
Log.d(TAG, "VPN permission required")
|
||||
|
||||
val intent = VpnService.prepare(activity)
|
||||
if (intent != null) {
|
||||
pendingArgs = args
|
||||
|
||||
startActivityForResult(
|
||||
invoke,
|
||||
permissionIntent,
|
||||
"onVpnPermissionResult"
|
||||
)
|
||||
|
||||
invoke.resolve()
|
||||
return
|
||||
startActivityForResult(invoke, intent, "onVpnPermissionResult")
|
||||
} else {
|
||||
startVpnService(args)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Permission already granted")
|
||||
|
||||
startVpnService(args)
|
||||
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
private fun startVpnService(args: VpnArgs) {
|
||||
Log.d(TAG, "Starting TunVpnService directly")
|
||||
|
||||
val intent = Intent(activity, TunVpnService::class.java).apply {
|
||||
action = "START_VPN"
|
||||
putExtra("ip", args.ip)
|
||||
putExtra("port", args.port)
|
||||
putExtra("config", args.config)
|
||||
}
|
||||
|
||||
// Для современных Android (API 26+) используем Foreground, для старых - обычный.
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
activity.startForegroundService(intent)
|
||||
} else {
|
||||
activity.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateStatus(status: String) {
|
||||
val data = JSObject()
|
||||
data.put("status", status)
|
||||
|
||||
Log.d(TAG, ">>> [PLUGIN INSTANCE] trigger() called with event: status-change, data: $status")
|
||||
|
||||
try {
|
||||
// Пробуем вызвать trigger. В Tauri 2.0 он должен прокинуть это в JS
|
||||
trigger("status-change", data)
|
||||
Log.d(TAG, ">>> [PLUGIN INSTANCE] trigger() executed successfully")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "!!! [PLUGIN INSTANCE] trigger() FAILED: ${e.message}", e)
|
||||
action = "START_VPN"; putExtra("ip", args.ip); putExtra("port", args.port); putExtra("config", args.config)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) activity.startForegroundService(intent)
|
||||
else activity.startService(intent)
|
||||
}
|
||||
|
||||
@ActivityCallback
|
||||
fun onVpnPermissionResult(invoke: Invoke, result: ActivityResult) {
|
||||
|
||||
Log.d(TAG, "VPN permission result received")
|
||||
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
|
||||
Log.d(TAG, "VPN permission granted")
|
||||
|
||||
pendingArgs?.let {
|
||||
startVpnService(it)
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
Log.w(TAG, "VPN permission denied")
|
||||
|
||||
invoke.reject("Permission denied")
|
||||
}
|
||||
|
||||
if (result.resultCode == Activity.RESULT_OK) pendingArgs?.let { startVpnService(it) }
|
||||
pendingArgs = null
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@Command
|
||||
fun stopVpn(invoke: Invoke) {
|
||||
|
||||
Log.d(TAG, "stopVpn command invoked")
|
||||
|
||||
val intent = Intent(activity, TunVpnService::class.java).apply {
|
||||
action = "STOP_VPN"
|
||||
}
|
||||
|
||||
activity.startService(intent)
|
||||
|
||||
Log.d(TAG, "STOP_VPN intent sent")
|
||||
|
||||
activity.startService(Intent(activity, TunVpnService::class.java).apply { action = "STOP_VPN" })
|
||||
invoke.resolve()
|
||||
}
|
||||
private fun isVpnProcessRunning(): Boolean {
|
||||
val manager = activity.getSystemService(android.content.Context.ACTIVITY_SERVICE) as android.app.ActivityManager
|
||||
val runningProcesses = manager.runningAppProcesses ?: return false
|
||||
|
||||
val vpnProcessName = "${activity.packageName}:vpn"
|
||||
|
||||
for (processInfo in runningProcesses) {
|
||||
if (processInfo.processName == vpnProcessName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Command
|
||||
fun checkActualVpnStatus(invoke: Invoke) {
|
||||
// Используем activity.getSystemService
|
||||
val cm = activity.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
var isVpnActive = false
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
val activeNetwork = cm.activeNetwork
|
||||
val capabilities = cm.getNetworkCapabilities(activeNetwork)
|
||||
// Проверяем наличие TRANSPORT_VPN
|
||||
isVpnActive = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ?: false
|
||||
}
|
||||
|
||||
val hasTunInterface = try {
|
||||
java.net.NetworkInterface.getNetworkInterfaces()?.asSequence()?.any {
|
||||
it.isUp && it.name.contains("tun")
|
||||
} ?: false
|
||||
} catch (e: Exception) { false }
|
||||
|
||||
val processAlive = isVpnProcessRunning()
|
||||
|
||||
// Если хоть один из признаков говорит, что VPN мертв — ставим idle
|
||||
val status = if (isVpnActive && hasTunInterface && processAlive) "connected" else "idle"
|
||||
|
||||
Log.d(TAG, "HARD CHECK: TransportVPN=$isVpnActive, Tun=$hasTunInterface, Proc=$processAlive -> Result=$status")
|
||||
|
||||
val res = JSObject()
|
||||
res.put("status", status)
|
||||
invoke.resolve(res)
|
||||
val status = getHardwareVpnStatus()
|
||||
invoke.resolve(JSObject().apply { put("status", status) })
|
||||
}
|
||||
|
||||
private fun updateStatus(status: String) {
|
||||
trigger("status-change", JSObject().apply { put("status", status) })
|
||||
}
|
||||
|
||||
@InvokeArg class VpnArgs { var config: TunnelConfig? = null; var ip: String = ""; var port: Int = 0 }
|
||||
}
|
||||
Reference in New Issue
Block a user