tg-ws-proxy/app/src/main/java/com/example/tgwsproxy/ProxyService.kt

197 lines
6.6 KiB
Kotlin

package com.example.tgwsproxy
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.os.Build
import android.os.IBinder
import android.os.PowerManager
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class ProxyService : Service() {
private var wakeLock: PowerManager.WakeLock? = null
private var statsJob: Job? = null
companion object {
const val ACTION_START = "com.example.tgwsproxy.START"
const val ACTION_STOP = "com.example.tgwsproxy.STOP"
const val EXTRA_PORT = "EXTRA_PORT"
const val EXTRA_IPS = "EXTRA_IPS"
const val EXTRA_POOL_SIZE = "EXTRA_POOL_SIZE"
private const val NOTIFICATION_ID = 1
private const val CHANNEL_ID = "ProxyServiceChannel"
private val _isRunning = MutableStateFlow(false)
val isRunning: StateFlow<Boolean> = _isRunning
}
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> {
val port = intent.getIntExtra(EXTRA_PORT, 8080)
val ips = intent.getStringExtra(EXTRA_IPS) ?: ""
val poolSize = intent.getIntExtra(EXTRA_POOL_SIZE, 4)
startProxy(port, ips, poolSize)
}
ACTION_STOP -> {
stopProxy()
}
}
return START_STICKY
}
private fun startProxy(port: Int, ips: String, poolSize: Int = 4) {
if (_isRunning.value) return
val notification = createNotification("Запуск прокси...")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
} else {
startForeground(NOTIFICATION_ID, notification)
}
acquireWakeLock()
Thread {
NativeProxy.setPoolSize(poolSize)
NativeProxy.startProxy("127.0.0.1", port, ips, 1)
}.start()
_isRunning.value = true
statsJob = CoroutineScope(Dispatchers.IO).launch {
while (isActive) {
delay(2000)
if (_isRunning.value) {
val rawStats = NativeProxy.getStats() ?: continue
val upRaw = extractStat(rawStats, "up=")
val downRaw = extractStat(rawStats, "down=")
val totalBytes = parseHumanBytes(upRaw) + parseHumanBytes(downRaw)
val text = "Трафик: ${formatBytes(totalBytes)}"
val manager = getSystemService(NotificationManager::class.java)
manager?.notify(NOTIFICATION_ID, createNotification(text))
}
}
}
}
private fun extractStat(stats: String, key: String): String {
val idx = stats.indexOf(key)
if (idx == -1) return "0B"
val start = idx + key.length
val end = stats.indexOf(" ", start)
return if (end == -1) stats.substring(start) else stats.substring(start, end)
}
private fun parseHumanBytes(s: String): Double {
val num = s.replace(Regex("[^0-9.]"), "").toDoubleOrNull() ?: 0.0
return when {
s.endsWith("TB") -> num * 1024.0 * 1024 * 1024 * 1024
s.endsWith("GB") -> num * 1024.0 * 1024 * 1024
s.endsWith("MB") -> num * 1024.0 * 1024
s.endsWith("KB") -> num * 1024.0
else -> num
}
}
private fun formatBytes(bytes: Double): String {
if (bytes < 1024) return "%.0fB".format(bytes)
if (bytes < 1024 * 1024) return "%.1fKB".format(bytes / 1024)
if (bytes < 1024 * 1024 * 1024) return "%.1fMB".format(bytes / (1024 * 1024))
return "%.2fGB".format(bytes / (1024 * 1024 * 1024))
}
private fun stopProxy() {
statsJob?.cancel()
statsJob = null
Thread { NativeProxy.stopProxy() }.start()
releaseWakeLock()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
stopForeground(true)
}
stopSelf()
_isRunning.value = false
}
private fun acquireWakeLock() {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"TgWsProxy::ServiceWakeLock"
)
wakeLock?.acquire()
}
private fun releaseWakeLock() {
try {
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
} catch (e: Exception) {
// Ignore wakelock exception
}
wakeLock = null
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Фоновый Прокси",
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(serviceChannel)
}
}
private fun createNotification(content: String): Notification {
val stopIntent = Intent(this, ProxyService::class.java).apply {
action = ACTION_STOP
}
val stopPendingIntent = PendingIntent.getService(
this, 0, stopIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Telegram WS Proxy")
.setContentText(content)
.setSmallIcon(R.drawable.ic_notification) // Local pure vector for Android 16 compatibility
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Отключить", stopPendingIntent)
.setOngoing(true)
.setOnlyAlertOnce(true) // prevent vibrate/sound on updates
.build()
}
override fun onDestroy() {
if (_isRunning.value) {
stopProxy()
}
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
}