Initial v1.1.8 Commits
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user