197 lines
6.6 KiB
Kotlin
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
|
|
}
|
|
}
|