Initial v1.1.8 Commits

This commit is contained in:
amurcanov
2026-05-23 22:18:08 +03:00
commit ac86caaf8b
76 changed files with 15693 additions and 0 deletions
@@ -0,0 +1,378 @@
package com.wdtt.client
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.util.Log
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
private const val TUNNEL_NOTIFICATION_CHANNEL_ID = "wdtt_tunnel_v4"
private const val TUNNEL_NOTIFICATION_ID = 1
class TunnelService : Service() {
private var wakeLock: PowerManager.WakeLock? = null
private var wifiLock: WifiManager.WifiLock? = null
private var updateJob: Job? = null
private var lastNotificationText: String? = null
// Network Monitoring
private var connectivityManager: ConnectivityManager? = null
private var networkCallback: ConnectivityManager.NetworkCallback? = null
private var lastNetworkChangeTime = 0L
private val activeNetworks = mutableSetOf<Network>()
private var isTunnelPaused = false
override fun onCreate() {
super.onCreate()
createNotificationChannel()
// Сразу берем лок при создании
acquireWakeLock()
setupNetworkCallback()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) {
restoreTunnel()
return START_STICKY
}
when (intent.action) {
"START" -> {
val notification = createNotification("Запуск...")
startPersistentForeground(notification)
val params = TunnelParams(
peer = intent.getStringExtra("peer") ?: "",
vkHashes = intent.getStringExtra("vk_hashes") ?: "",
secondaryVkHash = intent.getStringExtra("secondary_vk_hash") ?: "",
workersPerHash = intent.getIntExtra("workers_per_hash", 16),
port = intent.getIntExtra("port", 9000),
sni = intent.getStringExtra("sni") ?: "",
connectionPassword = intent.getStringExtra("connection_password") ?: "",
protocol = intent.getStringExtra("protocol") ?: "udp",
captchaMode = sanitizeCaptchaMode(intent.getStringExtra("captcha_mode")),
captchaSolveMethod = intent.getStringExtra("captcha_solve_method") ?: "auto"
)
startTunnel(params)
}
"STOP" -> stopTunnel()
"DEPLOY_START" -> {
val notification = createNotification("Установка на сервер...", "DEPLOY_CANCEL", "Отменить")
startPersistentForeground(notification)
acquireWakeLock()
}
"DEPLOY_CANCEL" -> {
com.wdtt.client.DeployManager.writeError("[!] ❌ Установка отменена пользователем")
com.wdtt.client.DeployManager.stopDeploy("error: Отменена пользователем")
stopForeground(STOP_FOREGROUND_REMOVE)
}
"DEPLOY_STOP" -> {
if (!TunnelManager.running.value) {
stopTunnel()
} else {
updateNotification("Туннель активен")
}
}
}
return START_STICKY
}
private fun restoreTunnel() {
val notification = createNotification("Восстановление соединения...")
startPersistentForeground(notification)
val appContext = applicationContext
TunnelManager.scope.launch {
try {
val store = SettingsStore(appContext)
val basePeer = store.peer.first()
val manualPortsEnabled = store.manualPortsEnabled.first()
val serverDtlsPort = if (manualPortsEnabled) store.serverDtlsPort.first() else 56000
val peerWithPort = if (basePeer.isBlank() || basePeer.contains(":")) basePeer else "$basePeer:$serverDtlsPort"
val params = TunnelParams(
peer = peerWithPort,
vkHashes = store.vkHashes.first(),
secondaryVkHash = store.secondaryVkHash.first(),
workersPerHash = store.workersPerHash.first(),
port = store.listenPort.first(),
sni = store.sni.first(),
connectionPassword = store.connectionPassword.first(),
captchaMode = sanitizeCaptchaMode(store.captchaMode.first()),
captchaSolveMethod = store.captchaSolveMethod.first()
)
if (params.peer.isNotEmpty() && params.vkHashes.isNotEmpty()) {
launch(Dispatchers.Main) {
startTunnel(params)
}
} else {
launch(Dispatchers.Main) {
stopTunnel()
}
}
} catch (e: Exception) {
launch(Dispatchers.Main) {
stopTunnel()
}
}
}
}
private fun startTunnel(params: TunnelParams) {
updateNotification("Подключение...")
acquireWakeLock()
acquireWifiLock()
// Подготавливаем CaptchaWebViewManager (не создаёт WebView — просто сохраняет контекст)
// Вызываем всегда — дёшево, а WebView создаётся на лету при каждом запросе капчи
CaptchaWebViewManager.onTunnelStart(applicationContext)
TunnelManager.start(this, params)
startStatsUpdater()
}
private fun stopTunnel() {
updateJob?.cancel()
// Уничтожаем текущий WebView (если капча решается) и чистим контекст
CaptchaWebViewManager.onTunnelStop()
TunnelManager.stop()
releaseWakeLock()
releaseWifiLock()
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
private fun setupNetworkCallback() {
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
activeNetworks.clear()
networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
val wasEmpty = activeNetworks.isEmpty()
activeNetworks.add(network)
if (wasEmpty) {
if (isTunnelPaused) {
isTunnelPaused = false
Log.d("TunnelService", "Сеть появилась, возобновляем туннель")
TunnelManager.resume()
updateNotification("Подключение...")
} else {
handleNetworkChange()
}
} else {
handleNetworkChange()
}
}
override fun onLost(network: Network) {
super.onLost(network)
activeNetworks.remove(network)
if (activeNetworks.isEmpty() && TunnelManager.running.value && !isTunnelPaused) {
isTunnelPaused = true
Log.d("TunnelService", "Сеть потеряна, приостанавливаем туннель")
TunnelManager.pause()
updateNotification("Ожидание сети (Фоновый сон)")
}
}
}
// ВАЖНО: Слушаем только реальные (не VPN) сети с доступом в интернет.
// Иначе интерфейс VPN (tun0) считается активной сетью, и при "Режиме полёта" activeNetworks не падает до 0.
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build()
connectivityManager?.registerNetworkCallback(request, networkCallback!!)
}
private fun handleNetworkChange() {
val now = System.currentTimeMillis()
if (now - lastNetworkChangeTime < 5000) return
lastNetworkChangeTime = now
if (TunnelManager.running.value && !isTunnelPaused) {
Log.d("TunnelService", "Сеть изменилась, мягкий перезапуск Go-клиента")
TunnelManager.restartTransport()
}
}
private fun sanitizeCaptchaMode(mode: String?): String {
return when (mode?.lowercase()) {
"auto" -> "auto"
"rjs" -> "rjs"
"wv" -> "wv"
else -> "auto"
}
}
private fun acquireWakeLock() {
if (wakeLock?.isHeld == true) return
val pm = getSystemService(POWER_SERVICE) as PowerManager
wakeLock = pm.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"wdtt:tunnel_cpu"
).apply {
setReferenceCounted(false)
acquire()
}
}
@Suppress("DEPRECATION")
private fun acquireWifiLock() {
if (wifiLock?.isHeld == true) return
val wm = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
// Используем WIFI_MODE_FULL_LOW_LATENCY для Android 10+,
// это предотвращает отключение радиомодуля при выключенном экране
val mode = if (Build.VERSION.SDK_INT >= 29) {
WifiManager.WIFI_MODE_FULL_LOW_LATENCY
} else {
WifiManager.WIFI_MODE_FULL_HIGH_PERF
}
wifiLock = wm.createWifiLock(mode, "wdtt:wifi_perf").apply {
setReferenceCounted(false)
acquire()
}
}
private fun releaseWakeLock() {
if (wakeLock?.isHeld == true) {
wakeLock?.release()
}
wakeLock = null
}
private fun releaseWifiLock() {
if (wifiLock?.isHeld == true) {
wifiLock?.release()
}
wifiLock = null
}
private fun startStatsUpdater() {
updateJob?.cancel()
updateJob = TunnelManager.scope.launch(Dispatchers.Main) {
delay(1000)
while (isActive) {
if (!TunnelManager.running.value && !isTunnelPaused) {
// Туннель полностью остановлен (не на паузе) — убиваем сервис
stopSelf()
break
}
if (!isTunnelPaused) {
updateNotification(buildTunnelNotificationText())
}
delay(2000)
}
}
}
private fun buildTunnelNotificationText(): String {
val statsText = TunnelManager.stats.value.trim()
return when {
statsText.isEmpty() -> "Туннель активен"
statsText == "Ожидание данных..." -> "Туннель активен"
else -> statsText
}
}
private fun createNotificationChannel() {
val channel = NotificationChannel(
TUNNEL_NOTIFICATION_CHANNEL_ID,
"WDTT Туннель",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Уведомление о работе туннеля"
setShowBadge(false)
// ВАЖНО: Разрешаем показывать на экране блокировки
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
setSound(null, null)
enableVibration(false)
}
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
private fun createNotification(text: String, actionName: String = "STOP", actionTitle: String = "Отключить"): Notification {
val openIntent = PendingIntent.getActivity(
this, 0,
Intent(this, MainActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val stopIntent = PendingIntent.getService(
this, if (actionName == "STOP") 1 else 2,
Intent(this, TunnelService::class.java).apply { action = actionName },
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
return NotificationCompat.Builder(this, TUNNEL_NOTIFICATION_CHANNEL_ID)
.setContentTitle("WDTT")
.setContentText(text)
.setSmallIcon(R.drawable.ic_stat_connected)
.setOngoing(true)
.setLocalOnly(true)
.setContentIntent(openIntent)
.addAction(R.drawable.ic_stop, actionTitle, stopIntent)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFAULT)
// ВАЖНО: Делаем уведомление публичным (видимым на локскрине)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
// Категория SERVICE помогает системе понять важность
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setOnlyAlertOnce(true) // Не издавать звук и не будить экран при обновлении статистики!
.setSilent(true) // Делаем тихим само уведомление
.setShowWhen(false)
.setUsesChronometer(false)
.setWhen(0L)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
private fun startPersistentForeground(notification: Notification) {
if (Build.VERSION.SDK_INT >= 34) {
startForeground(TUNNEL_NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(TUNNEL_NOTIFICATION_ID, notification)
}
}
private fun updateNotification(text: String) {
if (lastNotificationText == text) return
lastNotificationText = text
val notification = createNotification(text)
getSystemService(NotificationManager::class.java).notify(TUNNEL_NOTIFICATION_ID, notification)
}
override fun onDestroy() {
super.onDestroy()
networkCallback?.let {
connectivityManager?.unregisterNetworkCallback(it)
}
stopTunnel()
}
override fun onBind(intent: Intent?): IBinder? = null
}