plugin fixes

This commit is contained in:
2026-03-30 20:31:45 +07:00
parent 17649bd1c0
commit 0f7f01310e
@@ -44,6 +44,9 @@ data class TunnelConfig @JsonCreator constructor(
class TunVpnService : VpnService() {
private var activeSession: Session? = null
private var vpnInterface: ParcelFileDescriptor? = null
// ДОБАВЛЕНО: Корутина для статистики живет теперь здесь
private var statsJob: Job? = null
companion object {
const val ACTION_STOP_VPN = "app.netrunner.vpn.STOP_FROM_NOTIFICATION"
@@ -84,6 +87,9 @@ class TunVpnService : VpnService() {
activeSession = manager.spawnSession("$ip:$port", fd, cacheDir.absolutePath)
triggerVibration(true)
sendStatusBroadcast("connected")
// ДОБАВЛЕНО: Запускаем цикл статистики при успешном коннекте
startStatsLoop()
} catch (e: Exception) {
Log.e(TAG, "Rust fail", e)
stopServiceProperly()
@@ -91,6 +97,8 @@ class TunVpnService : VpnService() {
}
private fun stopServiceProperly() {
stopStatsLoop() // ДОБАВЛЕНО: Останавливаем цикл при дисконнекте
activeSession?.stop()
activeSession = null
try { vpnInterface?.close() } catch (e: Exception) { }
@@ -101,6 +109,45 @@ class TunVpnService : VpnService() {
stopSelf()
}
// --- НОВЫЙ БЛОК КОРУТИН ВНУТРИ СЕРВИСА ---
private fun startStatsLoop() {
if (statsJob?.isActive == true) return
Log.d(TAG, "Starting traffic stats loop IN SERVICE")
statsJob = CoroutineScope(Dispatchers.IO).launch {
while (isActive) {
try {
val stats = SessionManager().getTrafficStats()
sendStatsBroadcast(
stats.rxBytes.toLong(),
stats.txBytes.toLong(),
stats.rxPackets.toLong(),
stats.txPackets.toLong()
)
} catch (e: Exception) {
Log.e(TAG, "Stats loop error", e)
}
delay(1000)
}
}
}
private fun stopStatsLoop() {
statsJob?.cancel()
statsJob = null
}
private fun sendStatsBroadcast(rxBytes: Long, txBytes: Long, rxPackets: Long, txPackets: Long) {
sendBroadcast(Intent("com.netrunner.vpn.TRAFFIC_STATS_UPDATE").apply {
putExtra("rxBytes", rxBytes)
putExtra("txBytes", txBytes)
putExtra("rxPackets", rxPackets)
putExtra("txPackets", txPackets)
setPackage(packageName) // Безопасность: шлем только в свое приложение
})
}
// -----------------------------------------
private fun sendStatusBroadcast(status: String) {
sendBroadcast(Intent("com.netrunner.vpn.STATUS_UPDATE").apply {
putExtra("status", status)
@@ -132,23 +179,13 @@ class TunVpnService : VpnService() {
val stopIcon = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
android.graphics.drawable.Icon.createWithResource(this, android.R.drawable.ic_menu_close_clear_cancel)
} else {
null // Для совсем древних версий можно передать null или оставить старый метод
}
} else null
val stopAction = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && stopIcon != null) {
Notification.Action.Builder(
stopIcon,
"Отключить",
stopPI
).build()
Notification.Action.Builder(stopIcon, "Отключить", stopPI).build()
} else {
@Suppress("DEPRECATION")
Notification.Action(
android.R.drawable.ic_menu_close_clear_cancel,
"Отключить",
stopPI
)
Notification.Action(android.R.drawable.ic_menu_close_clear_cancel, "Отключить", stopPI)
}
val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -163,16 +200,12 @@ class TunVpnService : VpnService() {
val notification = builder
.setContentTitle("Netrunner VPN")
.setContentText("Защита активна")
.setSmallIcon(android.R.drawable.ic_lock_lock)
.setContentIntent(contentPI)
.setOngoing(true)
.addAction(stopAction)
.setColor(vpnBlack)
.setColorized(true)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setCategory(Notification.CATEGORY_SERVICE)
.build()
@@ -195,28 +228,19 @@ class TunVpnService : VpnService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val fixedDelay = 30L
val stepCount = 23
val timings = LongArray(stepCount * 2)
val amplitudes = IntArray(stepCount * 2)
for (i in 0 until stepCount) {
val progress = i.toFloat() / (stepCount - 1)
val activeTime = if (isAscending) {
(10 + (progress * 50)).toLong()
} else {
(60 - (progress * 50)).toLong()
}
val activeTime = if (isAscending) (10 + (progress * 50)).toLong() else (60 - (progress * 50)).toLong()
timings[i * 2] = fixedDelay
timings[i * 2 + 1] = activeTime
amplitudes[i * 2] = 0
amplitudes[i * 2 + 1] = 255
}
vibrator.vibrate(VibrationEffect.createWaveform(timings, amplitudes, -1))
} else {
@Suppress("DEPRECATION")
@@ -225,17 +249,33 @@ class TunVpnService : VpnService() {
}
override fun onDestroy() {
stopStatsLoop() // На всякий случай
super.onDestroy()
}
}
@TauriPlugin
class VpnPlugin(private val activity: Activity) : Plugin(activity) {
private var pendingArgs: VpnArgs? = null
private var statsJob: Job? = null // Переменная для управления корутиной статистики
private val statusReceiver = object : BroadcastReceiver() {
// ДОБАВЛЕНО: Теперь плагин слушает ДВА типа бродкастов (статус и трафик)
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.getStringExtra("status")?.let { updateStatus(it) }
when (intent?.action) {
"com.netrunner.vpn.STATUS_UPDATE" -> {
intent.getStringExtra("status")?.let { updateStatus(it) }
}
"com.netrunner.vpn.TRAFFIC_STATS_UPDATE" -> {
val result = JSObject().apply {
put("rxBytes", intent.getLongExtra("rxBytes", 0L))
put("txBytes", intent.getLongExtra("txBytes", 0L))
put("rxPackets", intent.getLongExtra("rxPackets", 0L))
put("txPackets", intent.getLongExtra("txPackets", 0L))
}
// Пересылаем в UI
trigger("traffic-stats", result)
}
}
}
}
@@ -244,7 +284,6 @@ class VpnPlugin(private val activity: Activity) : Plugin(activity) {
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)
}
}
@@ -254,9 +293,7 @@ class VpnPlugin(private val activity: Activity) : Plugin(activity) {
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)
val isFresh = hostsFile.exists() && (System.currentTimeMillis() - hostsFile.lastModified() < 24 * 60 * 60 * 1000)
if (isFresh) {
Log.d(TAG, "DNS: Blocklist is fresh, skipping download")
@@ -264,18 +301,12 @@ class VpnPlugin(private val activity: Activity) : Plugin(activity) {
}
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}")
@@ -287,16 +318,20 @@ class VpnPlugin(private val activity: Activity) : Plugin(activity) {
try { System.loadLibrary("netrunner_client") } catch (e: Exception) { Log.e(TAG, "Lib fail") }
requestNotificationPermission()
val filter = IntentFilter("com.netrunner.vpn.STATUS_UPDATE")
// ДОБАВЛЕНО: Регистрируем фильтр на оба действия
val filter = IntentFilter().apply {
addAction("com.netrunner.vpn.STATUS_UPDATE")
addAction("com.netrunner.vpn.TRAFFIC_STATS_UPDATE")
}
if (Build.VERSION.SDK_INT >= 33) {
activity.registerReceiver(statusReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
activity.registerReceiver(broadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
activity.registerReceiver(statusReceiver, filter)
activity.registerReceiver(broadcastReceiver, filter)
}
CoroutineScope(Dispatchers.IO).launch {
updateBlocklistIfNeeded()
}
CoroutineScope(Dispatchers.IO).launch { updateBlocklistIfNeeded() }
CoroutineScope(Dispatchers.Main).launch {
kotlinx.coroutines.delay(500)
@@ -365,51 +400,7 @@ class VpnPlugin(private val activity: Activity) : Plugin(activity) {
invoke.resolve(JSObject().apply { put("status", status) })
}
// --- НОВЫЙ БЛОК: УПРАВЛЕНИЕ КОРОТУИНОЙ СТАТИСТИКИ ---
private fun startStatsLoop() {
// Защита от запуска нескольких корутин одновременно
if (statsJob?.isActive == true) return
Log.d(TAG, "Starting traffic stats loop")
statsJob = CoroutineScope(Dispatchers.IO).launch {
while (isActive) {
try {
val stats = SessionManager().getTrafficStats()
val result = JSObject().apply {
put("rxBytes", stats.rxBytes.toLong())
put("txBytes", stats.txBytes.toLong())
put("rxPackets", stats.rxPackets.toLong())
put("txPackets", stats.txPackets.toLong())
}
// Отправляем данные на фронтенд
trigger("traffic-stats", result)
} catch (e: Exception) {
Log.e(TAG, "Stats loop error", e)
}
delay(1000) // Пауза в 1 секунду
}
}
}
private fun stopStatsLoop() {
Log.d(TAG, "Stopping traffic stats loop")
statsJob?.cancel()
statsJob = null
}
// Обновленный метод updateStatus
private fun updateStatus(status: String) {
if (status == "connected") {
startStatsLoop()
} else {
stopStatsLoop()
}
trigger("status-change", JSObject().apply { put("status", status) })
}