|
@@ -1,129 +1,214 @@
|
|
|
package app.xixi.nomo
|
|
package app.xixi.nomo
|
|
|
|
|
|
|
|
import android.app.Notification
|
|
import android.app.Notification
|
|
|
-import android.app.NotificationChannel
|
|
|
|
|
import android.app.NotificationManager
|
|
import android.app.NotificationManager
|
|
|
|
|
+import android.content.BroadcastReceiver
|
|
|
|
|
+import android.content.Context
|
|
|
import android.content.Intent
|
|
import android.content.Intent
|
|
|
-import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
|
|
|
|
-import android.net.VpnService
|
|
|
|
|
|
|
+import android.content.IntentFilter
|
|
|
|
|
+import android.content.pm.ServiceInfo
|
|
|
import android.os.Build
|
|
import android.os.Build
|
|
|
import android.os.Bundle
|
|
import android.os.Bundle
|
|
|
-import android.os.DeadObjectException
|
|
|
|
|
-import android.os.Handler
|
|
|
|
|
-import android.os.IBinder
|
|
|
|
|
-import android.os.Looper
|
|
|
|
|
-import android.os.Message
|
|
|
|
|
-import android.os.Messenger
|
|
|
|
|
import android.os.ParcelFileDescriptor
|
|
import android.os.ParcelFileDescriptor
|
|
|
|
|
+import android.os.PowerManager
|
|
|
import android.os.SystemClock
|
|
import android.os.SystemClock
|
|
|
-import androidx.core.app.NotificationCompat
|
|
|
|
|
|
|
+import androidx.core.content.ContextCompat
|
|
|
|
|
+import androidx.lifecycle.lifecycleScope
|
|
|
|
|
+import app.xixi.nomo.App.Companion.EXTRA_IS_FOREGROUND
|
|
|
import app.xixi.nomo.XRayApi.Companion.VPN_STATE_CONNECTED
|
|
import app.xixi.nomo.XRayApi.Companion.VPN_STATE_CONNECTED
|
|
|
import app.xixi.nomo.XRayApi.Companion.VPN_STATE_ERROR
|
|
import app.xixi.nomo.XRayApi.Companion.VPN_STATE_ERROR
|
|
|
|
|
+import app.xixi.nomo.XRayApi.Companion.VPN_STATE_IDLE
|
|
|
|
|
+import com.google.gson.Gson
|
|
|
|
|
+import com.tekartik.sqflite.Constant
|
|
|
import go.Seq
|
|
import go.Seq
|
|
|
import ixvpn_mobile.Ixvpn_mobile
|
|
import ixvpn_mobile.Ixvpn_mobile
|
|
|
import ixvpn_mobile.ProxyConnectorHandler
|
|
import ixvpn_mobile.ProxyConnectorHandler
|
|
|
|
|
+import kotlinx.coroutines.Job
|
|
|
|
|
+import kotlinx.coroutines.delay
|
|
|
|
|
+import kotlinx.coroutines.isActive
|
|
|
|
|
+import kotlinx.coroutines.launch
|
|
|
|
|
+import kotlinx.coroutines.sync.Mutex
|
|
|
|
|
+import kotlinx.coroutines.sync.withLock
|
|
|
|
|
+import org.json.JSONObject
|
|
|
|
|
+import win.fkey.netboost.service.NetworkReporter
|
|
|
|
|
|
|
|
-class XRayService : VpnService() {
|
|
|
|
|
|
|
|
|
|
- private val mMessenger = Messenger(IncomingHandler())
|
|
|
|
|
|
|
+class XRayService : LifecycleVpnService() {
|
|
|
private val tunnelService = TProxyService()
|
|
private val tunnelService = TProxyService()
|
|
|
- private var tunFileDescriptor: ParcelFileDescriptor? = null
|
|
|
|
|
- private var replyMessenger: Messenger? = null
|
|
|
|
|
|
|
+ private var tunFd: ParcelFileDescriptor? = null
|
|
|
|
|
+ private val mutex = Mutex()
|
|
|
|
|
|
|
|
- // 计时功能相关变量
|
|
|
|
|
- private val timerHandler = Handler(Looper.getMainLooper())
|
|
|
|
|
- private var isTimerRunning = false
|
|
|
|
|
- private var isTimerPaused = false
|
|
|
|
|
- private var timerMode = 0 // 0: 普通计时, 1: 倒计时
|
|
|
|
|
|
|
+ private var vpnStatus = VPN_STATE_IDLE
|
|
|
|
|
|
|
|
- // 使用 SystemClock.elapsedRealtime() 确保时间准确性(包括休眠时间)
|
|
|
|
|
|
|
+ // 计时功能相关变量
|
|
|
|
|
+ private var timerJob: Job? = null
|
|
|
|
|
+ private var timerMode = TIMER_MODE_NORMAL // 0: 普通计时, 1: 倒计时
|
|
|
private var timerBaseRealtime = 0L // 计时开始的真实时间(elapsedRealtime)
|
|
private var timerBaseRealtime = 0L // 计时开始的真实时间(elapsedRealtime)
|
|
|
private var timerInitialTime = 0L // 初始时间(对于倒计时是总时长,对于正常计时是0或已用时间)
|
|
private var timerInitialTime = 0L // 初始时间(对于倒计时是总时长,对于正常计时是0或已用时间)
|
|
|
private var timerPausedElapsed = 0L // 暂停时已经过的时间
|
|
private var timerPausedElapsed = 0L // 暂停时已经过的时间
|
|
|
|
|
+ private var isTimerPaused = false
|
|
|
|
|
|
|
|
- // 计时器Runnable
|
|
|
|
|
- private val timerRunnable = object : Runnable {
|
|
|
|
|
- override fun run() {
|
|
|
|
|
- if (isTimerRunning && !isTimerPaused) {
|
|
|
|
|
- // 计算从开始到现在经过的真实时间
|
|
|
|
|
- val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
|
|
|
|
|
|
|
+ private var xrayConfig: XrayConfig? = null
|
|
|
|
|
|
|
|
- val currentTime = if (timerMode == 0) {
|
|
|
|
|
- // 普通计时:初始时间 + 经过时间
|
|
|
|
|
- timerInitialTime + elapsedTime
|
|
|
|
|
- } else {
|
|
|
|
|
- // 倒计时:初始时间 - 经过时间
|
|
|
|
|
- timerInitialTime - elapsedTime
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ private var remainTime = 0L
|
|
|
|
|
|
|
|
- // 检查倒计时是否结束
|
|
|
|
|
- if (timerMode == 1 && currentTime <= 0) {
|
|
|
|
|
- VLog.i(TAG, "倒计时结束 - 关闭VPN (elapsed: ${elapsedTime}ms)")
|
|
|
|
|
- dealStopMsg()
|
|
|
|
|
- notifyStop()
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 屏幕状态跟踪
|
|
|
|
|
+ private var isScreenOn = true
|
|
|
|
|
+ private var screenReceiver: BroadcastReceiver? = null
|
|
|
|
|
|
|
|
- // 更新通知
|
|
|
|
|
- updateNotification(currentTime)
|
|
|
|
|
|
|
+ // 应用前后台状态跟踪
|
|
|
|
|
+ private var isAppForeground = true
|
|
|
|
|
+ private var foregroundReceiver: BroadcastReceiver? = null
|
|
|
|
|
|
|
|
- // 发送计时更新
|
|
|
|
|
- sendTimerUpdate(currentTime)
|
|
|
|
|
|
|
+ companion object {
|
|
|
|
|
+ private val TAG: String = XRayService::class.java.simpleName
|
|
|
|
|
+
|
|
|
|
|
+ private const val FOREGROUND_SERVICE_ID: Int = 1
|
|
|
|
|
+ private const val NOTIFICATION_CHANNEL_ID: String = "NOMO"
|
|
|
|
|
+
|
|
|
|
|
+ // 计时模式常量
|
|
|
|
|
+ const val TIMER_MODE_NORMAL = 0 // 普通计时(正计时)
|
|
|
|
|
+ const val TIMER_MODE_COUNTDOWN = 1 // 倒计时
|
|
|
|
|
+
|
|
|
|
|
+ private var serviceStatus: ServiceStatus = ServiceStatus.Disconnected
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onCreate() {
|
|
|
|
|
+ super.onCreate()
|
|
|
|
|
+ // 初始化文件日志系统
|
|
|
|
|
+ VLog.init(applicationContext)
|
|
|
|
|
+ VLog.i(TAG, "XRayService onCreate")
|
|
|
|
|
+
|
|
|
|
|
+ Seq.setContext(applicationContext)
|
|
|
|
|
+ Ixvpn_mobile.initProxyConnector()
|
|
|
|
|
+ registerNotificationChannel(
|
|
|
|
|
+ this,
|
|
|
|
|
+ NOTIFICATION_CHANNEL_ID,
|
|
|
|
|
+ "VPN",
|
|
|
|
|
+ )
|
|
|
|
|
+ // 注册屏幕状态监听
|
|
|
|
|
+ registerScreenReceiver()
|
|
|
|
|
+ // 注册应用前后台状态监听
|
|
|
|
|
+ registerForegroundReceiver()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 注册屏幕状态监听器
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun registerScreenReceiver() {
|
|
|
|
|
+ screenReceiver = object : BroadcastReceiver() {
|
|
|
|
|
+ override fun onReceive(context: Context, intent: Intent) {
|
|
|
|
|
+ when (intent.action) {
|
|
|
|
|
+ Intent.ACTION_SCREEN_ON -> {
|
|
|
|
|
+ isScreenOn = true
|
|
|
|
|
+ VLog.d(TAG, "屏幕亮起")
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 继续下一秒
|
|
|
|
|
- timerHandler.postDelayed(this, 1000L)
|
|
|
|
|
|
|
+ Intent.ACTION_SCREEN_OFF -> {
|
|
|
|
|
+ isScreenOn = false
|
|
|
|
|
+ VLog.d(TAG, "屏幕熄灭")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ val filter = IntentFilter().apply {
|
|
|
|
|
+ addAction(Intent.ACTION_SCREEN_ON)
|
|
|
|
|
+ addAction(Intent.ACTION_SCREEN_OFF)
|
|
|
|
|
+ }
|
|
|
|
|
+ registerReceiver(screenReceiver, filter)
|
|
|
|
|
+
|
|
|
|
|
+ // 获取初始屏幕状态
|
|
|
|
|
+ val powerManager = getSystemService(POWER_SERVICE) as PowerManager
|
|
|
|
|
+ isScreenOn = powerManager.isInteractive
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private val vpnHandler = object : ProxyConnectorHandler {
|
|
|
|
|
- override fun proxyStatusChange(l: Long, msg: String) {
|
|
|
|
|
- VLog.i(TAG, "status:$l msg:$msg")
|
|
|
|
|
- // 发送状态消息到XRayApi
|
|
|
|
|
- sendStatusMessage(l, msg)
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 注销屏幕状态监听器
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun unregisterScreenReceiver() {
|
|
|
|
|
+ screenReceiver?.let {
|
|
|
|
|
+ try {
|
|
|
|
|
+ unregisterReceiver(it)
|
|
|
|
|
+ } catch (e: Exception) {
|
|
|
|
|
+ VLog.e(TAG, "注销屏幕监听器失败", e)
|
|
|
|
|
+ }
|
|
|
|
|
+ screenReceiver = null
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private inner class IncomingHandler : Handler() {
|
|
|
|
|
- override fun handleMessage(msg: Message) {
|
|
|
|
|
- handleRemoteMessage(msg)
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 注册应用前后台状态监听器
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun registerForegroundReceiver() {
|
|
|
|
|
+ foregroundReceiver = object : BroadcastReceiver() {
|
|
|
|
|
+ override fun onReceive(context: Context, intent: Intent) {
|
|
|
|
|
+ if (intent.action == APP_FOREGROUND_BROADCAST) {
|
|
|
|
|
+ isAppForeground = intent.getBooleanExtra(EXTRA_IS_FOREGROUND, true)
|
|
|
|
|
+ VLog.d(TAG, "应用前后台状态变化: ${if (isAppForeground) "前台" else "后台"}")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ val filter = IntentFilter(APP_FOREGROUND_BROADCAST)
|
|
|
|
|
+ ContextCompat.registerReceiver(
|
|
|
|
|
+ this,
|
|
|
|
|
+ foregroundReceiver,
|
|
|
|
|
+ filter,
|
|
|
|
|
+ ContextCompat.RECEIVER_NOT_EXPORTED
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- override fun onBind(intent: Intent): IBinder? {
|
|
|
|
|
- // 首先检查是否是 VPN 服务的绑定请求
|
|
|
|
|
- if (SERVICE_INTERFACE == intent.action) {
|
|
|
|
|
- return super.onBind(intent)
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 注销应用前后台状态监听器
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun unregisterForegroundReceiver() {
|
|
|
|
|
+ foregroundReceiver?.let {
|
|
|
|
|
+ try {
|
|
|
|
|
+ unregisterReceiver(it)
|
|
|
|
|
+ } catch (e: Exception) {
|
|
|
|
|
+ VLog.e(TAG, "注销前后台监听器失败", e)
|
|
|
|
|
+ }
|
|
|
|
|
+ foregroundReceiver = null
|
|
|
}
|
|
}
|
|
|
- // 其他绑定请求返回 Messenger
|
|
|
|
|
- return mMessenger.binder
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- override fun onCreate() {
|
|
|
|
|
- super.onCreate()
|
|
|
|
|
- // 初始化文件日志系统
|
|
|
|
|
- VLog.init(applicationContext)
|
|
|
|
|
- VLog.i(TAG, "XRayService onCreate")
|
|
|
|
|
|
|
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
|
|
|
+ super.onStartCommand(intent, flags, startId)
|
|
|
|
|
+ return when (val action = intent?.action) {
|
|
|
|
|
+ START_ACTION -> {
|
|
|
|
|
+ lifecycleScope.launch { intent.extras?.let { start(it) } }
|
|
|
|
|
+ START_STICKY
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- Seq.setContext(applicationContext)
|
|
|
|
|
- Ixvpn_mobile.initProxyConnector()
|
|
|
|
|
- createNotificationChannel()
|
|
|
|
|
|
|
+ STOP_ACTION -> {
|
|
|
|
|
+ VLog.i(TAG, "onStartCommand")
|
|
|
|
|
+ lifecycleScope.launch {
|
|
|
|
|
+ stop()
|
|
|
|
|
+ }
|
|
|
|
|
+ START_NOT_STICKY
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ else -> {
|
|
|
|
|
+ VLog.i(TAG, "Unknown action: $action")
|
|
|
|
|
+ START_NOT_STICKY
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onRevoke() {
|
|
override fun onRevoke() {
|
|
|
- super.onRevoke()
|
|
|
|
|
VLog.i(TAG, "onRevoke")
|
|
VLog.i(TAG, "onRevoke")
|
|
|
- // 停止计时
|
|
|
|
|
- sendStatusMessage(VPN_STATE_ERROR, "onRevoke")
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- override fun onTaskRemoved(rootIntent: Intent?) {
|
|
|
|
|
- super.onTaskRemoved(rootIntent)
|
|
|
|
|
- VLog.i(TAG, "onTaskRemoved")
|
|
|
|
|
|
|
+ lifecycleScope.launch { stop() }
|
|
|
|
|
+ sendStatusMessage(VPN_STATE_ERROR, ERROR_REVOKE, "System cleanup")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onDestroy() {
|
|
override fun onDestroy() {
|
|
|
- VLog.i(TAG, "onDestroy")
|
|
|
|
|
|
|
+ VLog.i(TAG, "Service onDestroy")
|
|
|
|
|
+ // 确保资源被清理
|
|
|
|
|
+ // 注销屏幕状态监听
|
|
|
|
|
+ unregisterScreenReceiver()
|
|
|
|
|
+ // 注销前后台监听
|
|
|
|
|
+ unregisterForegroundReceiver()
|
|
|
// 停止计时
|
|
// 停止计时
|
|
|
stopTimer()
|
|
stopTimer()
|
|
|
// 先停止前台服务
|
|
// 先停止前台服务
|
|
@@ -133,10 +218,6 @@ class XRayService : VpnService() {
|
|
|
VLog.e(TAG, "stopForeground 失败", e)
|
|
VLog.e(TAG, "stopForeground 失败", e)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 清理Messenger引用
|
|
|
|
|
- replyMessenger = null
|
|
|
|
|
- VLog.i(TAG, "replyMessenger 已清理")
|
|
|
|
|
-
|
|
|
|
|
// 清理资源
|
|
// 清理资源
|
|
|
try {
|
|
try {
|
|
|
Ixvpn_mobile.freeProxyConnector()
|
|
Ixvpn_mobile.freeProxyConnector()
|
|
@@ -146,97 +227,208 @@ class XRayService : VpnService() {
|
|
|
|
|
|
|
|
// 关闭日志系统
|
|
// 关闭日志系统
|
|
|
VLog.close()
|
|
VLog.close()
|
|
|
-
|
|
|
|
|
super.onDestroy()
|
|
super.onDestroy()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
|
|
|
- createNotification()
|
|
|
|
|
- return START_STICKY
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ private suspend fun start(bundle: Bundle) {
|
|
|
|
|
+ VLog.i(TAG, "Starting")
|
|
|
|
|
|
|
|
- private fun handleRemoteMessage(msg: Message) {
|
|
|
|
|
- // 只在有replyTo时才记录Messenger对象
|
|
|
|
|
- if (msg.replyTo != null) {
|
|
|
|
|
- replyMessenger = msg.replyTo
|
|
|
|
|
- VLog.i(TAG, "记录replyTo Messenger: ${msg.what}")
|
|
|
|
|
- } else {
|
|
|
|
|
- VLog.w(TAG, "消息没有replyTo,无法记录Messenger")
|
|
|
|
|
|
|
+ if (serviceStatus == ServiceStatus.Connected) {
|
|
|
|
|
+ VLog.i(TAG, "VPN already connected")
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- when (msg.what) {
|
|
|
|
|
- XRAY_MSG_START -> dealStartMsg(msg)
|
|
|
|
|
- XRAY_MSG_STOP -> dealStopMsg()
|
|
|
|
|
|
|
+ val configJson = bundle.getString("config")
|
|
|
|
|
+ xrayConfig = Gson().fromJson(configJson, XrayConfig::class.java)
|
|
|
|
|
+
|
|
|
|
|
+ if(xrayConfig == null) {
|
|
|
|
|
+ updateStatus(ServiceStatus.Failed)
|
|
|
|
|
+ sendStatusMessage(VPN_STATE_ERROR, ERROR_INIT, "参数异常")
|
|
|
|
|
+ stopSelfService()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ remainTime = xrayConfig!!.remainTime * 1000L
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ mutex.withLock {
|
|
|
|
|
+ startTun2Socks()
|
|
|
|
|
+ }
|
|
|
|
|
+ updateStatus(ServiceStatus.Connected)
|
|
|
|
|
+ startForeground()
|
|
|
|
|
+ } catch (e: Exception) {
|
|
|
|
|
+ VLog.e(TAG, "Failed to start VPN", e)
|
|
|
|
|
+ updateStatus(ServiceStatus.Failed)
|
|
|
|
|
+ stop()
|
|
|
|
|
+ sendStatusMessage(VPN_STATE_ERROR, ERROR_INIT, "Failed to startTun2Socks")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private fun dealStartMsg(msg: Message) {
|
|
|
|
|
- VLog.i(TAG, "deal start xray msg")
|
|
|
|
|
- val bundle = msg.data
|
|
|
|
|
- val socksPort = bundle.getInt("socksPort")
|
|
|
|
|
- val startOptions = bundle.getString("startOptions")
|
|
|
|
|
- val sessionId = bundle.getString("sessionId")
|
|
|
|
|
- val tunnelConfig = bundle.getString("tunnelConfig")
|
|
|
|
|
- val allowVpnApps = bundle.getStringArrayList("allowVpnApps") ?: arrayListOf()
|
|
|
|
|
- val disallowVpnApps = bundle.getStringArrayList("disallowVpnApps") ?: arrayListOf()
|
|
|
|
|
-
|
|
|
|
|
- VLog.i(TAG, "socksPort: $socksPort, sessionId: $sessionId")
|
|
|
|
|
|
|
+ private fun startTun2Socks() {
|
|
|
|
|
+ VLog.i(TAG, "socksPort: ${xrayConfig?.socksPort}, sessionId: ${xrayConfig?.sessionId}")
|
|
|
VLog.i(
|
|
VLog.i(
|
|
|
TAG,
|
|
TAG,
|
|
|
- "allowVpnApps count: ${allowVpnApps.size}, disallowVpnApps count: ${disallowVpnApps.size}"
|
|
|
|
|
|
|
+ "allowVpnApps count: ${xrayConfig?.allowVpnApps?.size}, disallowVpnApps count: ${xrayConfig?.disallowVpnApps?.size}"
|
|
|
)
|
|
)
|
|
|
|
|
+ val builder = Builder()
|
|
|
|
|
+ val ipAddress = "192.168.34.2"
|
|
|
|
|
+ builder.setSession("ix_vpn")
|
|
|
|
|
+ .setMtu(1500)
|
|
|
|
|
+ .addAddress(ipAddress, 24)
|
|
|
|
|
+ .addRoute("0.0.0.0", 0)
|
|
|
|
|
+ .addDnsServer("8.8.8.8")
|
|
|
|
|
+
|
|
|
|
|
+ if (xrayConfig?.allowVpnApps!!.isNotEmpty()) {
|
|
|
|
|
+ xrayConfig?.allowVpnApps!!.forEach { app ->
|
|
|
|
|
+ builder.addAllowedApplication(app)
|
|
|
|
|
+ }
|
|
|
|
|
+ VLog.i(TAG, "允许的应用: ${xrayConfig?.allowVpnApps?.joinToString(", ")}")
|
|
|
|
|
+ } else {
|
|
|
|
|
+ xrayConfig?.disallowVpnApps!!.forEach { app ->
|
|
|
|
|
+ builder.addDisallowedApplication(app)
|
|
|
|
|
+ }
|
|
|
|
|
+ builder.addDisallowedApplication(packageName)
|
|
|
|
|
+ VLog.i(TAG, "禁止的应用: ${xrayConfig?.disallowVpnApps?.joinToString(", ")}")
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ // 初始化网络请求参数
|
|
|
|
|
+ NetworkReporter.getInstance().initialize(xrayConfig!!)
|
|
|
|
|
+
|
|
|
|
|
+ tunFd = builder.establish()
|
|
|
|
|
+ tunFd?.let { tfd ->
|
|
|
|
|
+ VLog.i(TAG, "VPN tunnel established, fd: ${tfd.fd}")
|
|
|
|
|
+ tunnelService.startTunnel(this, xrayConfig!!.socksPort, xrayConfig!!.tunnelConfig, tfd.fd)
|
|
|
|
|
+ Ixvpn_mobile.proxyConnectorStart(xrayConfig!!.sessionId, xrayConfig!!.startOptions, vpnHandler)
|
|
|
|
|
+ VLog.i(TAG, "XRay proxy started successfully")
|
|
|
|
|
+ } ?: run {
|
|
|
|
|
+ VLog.e(TAG, "Failed to establish VPN tunnel")
|
|
|
|
|
+ updateStatus(ServiceStatus.Failed)
|
|
|
|
|
+ // 通知 Flutter 层启动失败
|
|
|
|
|
+ sendStatusMessage(VPN_STATE_ERROR, ERROR_INIT, "Failed to establish VPN tunnel")
|
|
|
|
|
+ stopSelfService()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private val vpnHandler = ProxyConnectorHandler { status, params ->
|
|
|
|
|
+ VLog.i(TAG, "status:$status params:$params")
|
|
|
|
|
+ if (vpnStatus == status) {
|
|
|
|
|
+ return@ProxyConnectorHandler
|
|
|
|
|
+ }
|
|
|
|
|
+ vpnStatus = status
|
|
|
|
|
+
|
|
|
|
|
+ var code = 0L
|
|
|
|
|
+ var message = ""
|
|
|
try {
|
|
try {
|
|
|
- val builder = Builder()
|
|
|
|
|
- val ipAddress = "192.168.34.2"
|
|
|
|
|
- builder.setSession("ixvpn")
|
|
|
|
|
- .setMtu(1500)
|
|
|
|
|
- .addAddress(ipAddress, 24)
|
|
|
|
|
- .addRoute("0.0.0.0", 0)
|
|
|
|
|
- .addDnsServer("8.8.8.8")
|
|
|
|
|
-
|
|
|
|
|
- if (allowVpnApps.isNotEmpty()) {
|
|
|
|
|
- allowVpnApps.forEach { app ->
|
|
|
|
|
- builder.addAllowedApplication(app)
|
|
|
|
|
- }
|
|
|
|
|
- VLog.i(TAG, "允许的应用: ${allowVpnApps.joinToString(", ")}")
|
|
|
|
|
- } else {
|
|
|
|
|
- disallowVpnApps.forEach { app ->
|
|
|
|
|
- builder.addDisallowedApplication(app)
|
|
|
|
|
- }
|
|
|
|
|
- builder.addDisallowedApplication(packageName)
|
|
|
|
|
- VLog.i(TAG, "禁止的应用: ${disallowVpnApps.joinToString(", ")}")
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ val json = JSONObject(params)
|
|
|
|
|
+ code = json.optLong("code", 0)
|
|
|
|
|
+ message = json.optString("message", "")
|
|
|
|
|
+ } catch (e: Exception) {
|
|
|
|
|
+ VLog.e(TAG, "解析 params JSON 失败", e)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- tunFileDescriptor = builder.establish()
|
|
|
|
|
- tunFileDescriptor?.let { tfd ->
|
|
|
|
|
- VLog.i(TAG, "VPN tunnel established, fd: ${tfd.fd}")
|
|
|
|
|
- tunnelService.startTunnel(this, socksPort, tunnelConfig, tfd.fd)
|
|
|
|
|
- Ixvpn_mobile.proxyConnectorStart(sessionId, startOptions, vpnHandler)
|
|
|
|
|
- VLog.i(TAG, "XRay proxy started successfully")
|
|
|
|
|
- } ?: run {
|
|
|
|
|
- VLog.e(TAG, "Failed to establish VPN tunnel")
|
|
|
|
|
- // 通知 Flutter 层启动失败
|
|
|
|
|
- sendStatusMessage(VPN_STATE_ERROR, "Failed to establish VPN tunnel")
|
|
|
|
|
|
|
+ // VPN 连接成功后启动计时
|
|
|
|
|
+ if (status == VPN_STATE_CONNECTED) {
|
|
|
|
|
+ val mode = if (xrayConfig!!.isCountdown) TIMER_MODE_COUNTDOWN else TIMER_MODE_NORMAL
|
|
|
|
|
+ val time = if (xrayConfig!!.isCountdown) remainTime else 0L
|
|
|
|
|
+ startTimer(mode, time)
|
|
|
|
|
+ uploadBoostResult(true, code)
|
|
|
|
|
+ AppLogger.getInstance(this).updateAppJsonStatusInfo(true, code)
|
|
|
|
|
+ dealStat()
|
|
|
|
|
+ } else if (status == VPN_STATE_ERROR) {
|
|
|
|
|
+ AppLogger.getInstance(this).updateAppJsonStatusInfo(false, code)
|
|
|
|
|
+ uploadBoostResult(false, code)
|
|
|
|
|
+ VLog.i(TAG, "vpnHandler")
|
|
|
|
|
+ lifecycleScope.launch { stop() }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 发送状态消息到XRayApi
|
|
|
|
|
+
|
|
|
|
|
+ sendStatusMessage(status, code, message)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun uploadBoostResult(success: Boolean, code: Long) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 更新 实时流量
|
|
|
|
|
+ val queryTypes = byteArrayOf(1)
|
|
|
|
|
+ val stats: String? = Ixvpn_mobile.proxyConnectorQueryStats(queryTypes)
|
|
|
|
|
+ val session: String = xrayConfig!!.sessionId
|
|
|
|
|
+ NetworkReporter.getInstance().addBoostResultParams(
|
|
|
|
|
+ "NM_BoostResult",
|
|
|
|
|
+ stats,
|
|
|
|
|
+ session,
|
|
|
|
|
+ success,
|
|
|
|
|
+ code
|
|
|
|
|
+ )
|
|
|
|
|
+ val param = NetworkReporter.getInstance()
|
|
|
|
|
+ .buildRequestParams("NM_BoostResult")
|
|
|
|
|
+ val intent = Intent(
|
|
|
|
|
+ BOOST_RESULT_BROADCAST
|
|
|
|
|
+ ).apply {
|
|
|
|
|
+ setPackage(packageName)
|
|
|
|
|
+ putExtra("param", param)
|
|
|
|
|
+ putExtra("success", success)
|
|
|
|
|
+ putExtra("locationCode", NetworkReporter.getInstance().getLocationCode())
|
|
|
|
|
+ putExtra("nodeId", NetworkReporter.getInstance().getConnectedNodeId())
|
|
|
}
|
|
}
|
|
|
- } catch (e: Exception) {
|
|
|
|
|
- VLog.e(TAG, "启动 XRay 失败", e)
|
|
|
|
|
- // 通知 Flutter 层启动失败
|
|
|
|
|
- sendStatusMessage(VPN_STATE_ERROR, "启动 XRay 失败: ${e.message}")
|
|
|
|
|
|
|
+ sendBroadcast(intent)
|
|
|
|
|
+ } catch (e: java.lang.Exception) {
|
|
|
|
|
+ VLog.e(TAG, "uploadBoostResult error: ${e.message}")
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private fun dealStopMsg() {
|
|
|
|
|
- VLog.i(TAG, "deal stop xray msg")
|
|
|
|
|
- doStop()
|
|
|
|
|
|
|
+ private fun sendStatusMessage(status: Long, code: Long, message: String) {
|
|
|
|
|
+ VLog.i(TAG, "sendStatusMessage status:$status code:$code message:$message")
|
|
|
|
|
+ val intent = Intent(STATUS_BROADCAST).apply {
|
|
|
|
|
+ setPackage(packageName) // 必须设置包名,否则 RECEIVER_NOT_EXPORTED 接收不到
|
|
|
|
|
+ putExtra("status", status)
|
|
|
|
|
+ putExtra("code", code)
|
|
|
|
|
+ putExtra("message", message)
|
|
|
|
|
+ }
|
|
|
|
|
+ sendBroadcast(intent)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private fun doStop() {
|
|
|
|
|
- VLog.i(TAG, "开始停止 XRay 服务")
|
|
|
|
|
|
|
+ private fun startForeground() {
|
|
|
|
|
+ val notification: Notification = createNotification()
|
|
|
|
|
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
|
|
|
+ startForeground(
|
|
|
|
|
+ FOREGROUND_SERVICE_ID,
|
|
|
|
|
+ notification,
|
|
|
|
|
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
|
|
|
|
|
+ )
|
|
|
|
|
+ } else {
|
|
|
|
|
+ startForeground(FOREGROUND_SERVICE_ID, notification)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private suspend fun stop() {
|
|
|
|
|
+ VLog.i(TAG, "Stopping")
|
|
|
|
|
+
|
|
|
|
|
+ mutex.withLock {
|
|
|
|
|
+ stopTun2Socks()
|
|
|
|
|
+ }
|
|
|
|
|
+ stopSelfService()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun stopSelfService() {
|
|
|
|
|
+ VLog.i(TAG, "stop self service")
|
|
|
|
|
+ updateStatus(ServiceStatus.Disconnected)
|
|
|
|
|
+ stopSelf()
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ private fun stopTun2Socks() {
|
|
|
// 停止计时
|
|
// 停止计时
|
|
|
stopTimer()
|
|
stopTimer()
|
|
|
|
|
|
|
|
|
|
+ tunFd?.let { tfd ->
|
|
|
|
|
+ try {
|
|
|
|
|
+ tfd.close()
|
|
|
|
|
+ VLog.i(TAG, "TUN file descriptor closed")
|
|
|
|
|
+ } catch (e: Exception) {
|
|
|
|
|
+ VLog.e(TAG, "关闭 TUN file descriptor 失败", e)
|
|
|
|
|
+ }
|
|
|
|
|
+ tunFd = null
|
|
|
|
|
+ }
|
|
|
|
|
+ VLog.i(TAG, "xray stopped")
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
tunnelService.stopTunnel()
|
|
tunnelService.stopTunnel()
|
|
|
VLog.i(TAG, "Tunnel stopped")
|
|
VLog.i(TAG, "Tunnel stopped")
|
|
@@ -250,243 +442,212 @@ class XRayService : VpnService() {
|
|
|
} catch (e: Exception) {
|
|
} catch (e: Exception) {
|
|
|
VLog.e(TAG, "停止 proxy connector 失败", e)
|
|
VLog.e(TAG, "停止 proxy connector 失败", e)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- tunFileDescriptor?.let { tfd ->
|
|
|
|
|
- try {
|
|
|
|
|
- tfd.close()
|
|
|
|
|
- VLog.i(TAG, "TUN file descriptor closed")
|
|
|
|
|
- } catch (e: Exception) {
|
|
|
|
|
- VLog.e(TAG, "关闭 TUN file descriptor 失败", e)
|
|
|
|
|
- }
|
|
|
|
|
- tunFileDescriptor = null
|
|
|
|
|
- }
|
|
|
|
|
- VLog.i(TAG, "xray stopped")
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private fun notifyStop() {
|
|
|
|
|
- replyMessenger?.let { messenger ->
|
|
|
|
|
- try {
|
|
|
|
|
- val msg = Message.obtain().apply {
|
|
|
|
|
- what = XRAY_MSG_REPLY_STOP
|
|
|
|
|
- }
|
|
|
|
|
- messenger.send(msg)
|
|
|
|
|
- VLog.i(TAG, "停止消息已通过Messenger发送")
|
|
|
|
|
- } catch (e: DeadObjectException) {
|
|
|
|
|
- VLog.w(TAG, "Messenger已失效,清理引用", e)
|
|
|
|
|
- replyMessenger = null
|
|
|
|
|
- } catch (e: Exception) {
|
|
|
|
|
- VLog.e(TAG, "发送Messenger停止消息失败", e)
|
|
|
|
|
- }
|
|
|
|
|
- } ?: run {
|
|
|
|
|
- VLog.w(TAG, "replyMessenger为空,无法发送停止消息")
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- private fun sendStatusMessage(status: Long, message: String) {
|
|
|
|
|
- try {
|
|
|
|
|
- if (status == VPN_STATE_CONNECTED) {
|
|
|
|
|
- // 启动计时(普通计时模式)
|
|
|
|
|
- startTimer(0, 0)
|
|
|
|
|
-// startTimer(1, 10000L)
|
|
|
|
|
- } else if (status == VPN_STATE_ERROR) {
|
|
|
|
|
- VLog.i(TAG, "VPN 连接失败,status:$status")
|
|
|
|
|
- }
|
|
|
|
|
- replyMessenger?.let { messenger ->
|
|
|
|
|
- try {
|
|
|
|
|
- val msg = Message.obtain().apply {
|
|
|
|
|
- what = XRAY_MSG_REPLY_STATUS
|
|
|
|
|
- // 使用Bundle传递消息,避免跨进程序列化问题
|
|
|
|
|
- data = Bundle().apply {
|
|
|
|
|
- putLong("status", status)
|
|
|
|
|
- putString("message", message)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- messenger.send(msg)
|
|
|
|
|
- VLog.i(TAG, "状态消息已通过Messenger发送: status=$status, message=$message")
|
|
|
|
|
- } catch (e: DeadObjectException) {
|
|
|
|
|
- VLog.w(TAG, "Messenger已失效,清理引用", e)
|
|
|
|
|
- replyMessenger = null
|
|
|
|
|
- } catch (e: Exception) {
|
|
|
|
|
- VLog.e(TAG, "发送Messenger状态消息失败", e)
|
|
|
|
|
- }
|
|
|
|
|
- } ?: run {
|
|
|
|
|
- VLog.w(TAG, "replyMessenger为空,无法发送状态消息")
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e: Exception) {
|
|
|
|
|
- VLog.e(TAG, "发送状态消息失败", e)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ private fun updateStatus(newStatus: ServiceStatus) {
|
|
|
|
|
+ VLog.d(TAG, "VPN status changed from $serviceStatus to $newStatus")
|
|
|
|
|
+ serviceStatus = newStatus
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 计时相关方法
|
|
|
|
|
|
|
+ private fun createNotification(): Notification =
|
|
|
|
|
+ createConnectionNotification(
|
|
|
|
|
+ this,
|
|
|
|
|
+ NOTIFICATION_CHANNEL_ID,
|
|
|
|
|
+ "NOMO VPN",
|
|
|
|
|
+ "Service is running",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+// ==================== 计时功能 ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 启动计时
|
|
|
|
|
+ * @param mode 计时模式: TIMER_MODE_NORMAL(0)=普通计时, TIMER_MODE_COUNTDOWN(1)=倒计时
|
|
|
|
|
+ * @param initialTime 初始时间(毫秒),普通计时为0,倒计时为总时长
|
|
|
|
|
+ */
|
|
|
private fun startTimer(mode: Int, initialTime: Long) {
|
|
private fun startTimer(mode: Int, initialTime: Long) {
|
|
|
- if (isTimerRunning)
|
|
|
|
|
|
|
+ if (timerJob?.isActive == true) {
|
|
|
|
|
+ VLog.w(TAG, "计时器已在运行")
|
|
|
return
|
|
return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
VLog.i(TAG, "启动计时: mode=$mode, initialTime=$initialTime")
|
|
VLog.i(TAG, "启动计时: mode=$mode, initialTime=$initialTime")
|
|
|
timerMode = mode
|
|
timerMode = mode
|
|
|
timerInitialTime = initialTime
|
|
timerInitialTime = initialTime
|
|
|
timerBaseRealtime = SystemClock.elapsedRealtime()
|
|
timerBaseRealtime = SystemClock.elapsedRealtime()
|
|
|
timerPausedElapsed = 0L
|
|
timerPausedElapsed = 0L
|
|
|
-
|
|
|
|
|
- isTimerRunning = true
|
|
|
|
|
isTimerPaused = false
|
|
isTimerPaused = false
|
|
|
|
|
|
|
|
- // 开始计时循环
|
|
|
|
|
- timerHandler.post(timerRunnable)
|
|
|
|
|
|
|
+ timerJob = lifecycleScope.launch {
|
|
|
|
|
+ while (isActive && !isTimerPaused) {
|
|
|
|
|
+ // 计算从开始到现在经过的真实时间
|
|
|
|
|
+ val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
|
|
|
|
|
+
|
|
|
|
|
+ val currentTime = if (timerMode == TIMER_MODE_NORMAL) {
|
|
|
|
|
+ // 普通计时:初始时间 + 经过时间
|
|
|
|
|
+ timerInitialTime + elapsedTime
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 倒计时:初始时间 - 经过时间
|
|
|
|
|
+ timerInitialTime - elapsedTime
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查倒计时是否结束
|
|
|
|
|
+ if (timerMode == TIMER_MODE_COUNTDOWN && currentTime <= 0) {
|
|
|
|
|
+ VLog.i(TAG, "倒计时结束 - 关闭VPN (elapsed: ${elapsedTime}ms)")
|
|
|
|
|
+ sendTimerUpdate(0L)
|
|
|
|
|
+ stop()
|
|
|
|
|
+ sendStatusMessage(VPN_STATE_ERROR, ERROR_REMAIN_TIME, "No available time")
|
|
|
|
|
+ return@launch
|
|
|
|
|
+ }
|
|
|
|
|
+ if (timerMode == TIMER_MODE_NORMAL && currentTime >= remainTime) {
|
|
|
|
|
+ VLog.i(TAG, "可用时间已结束 - 关闭VPN (elapsed: ${elapsedTime}ms)")
|
|
|
|
|
+ sendTimerUpdate(0L)
|
|
|
|
|
+ stop()
|
|
|
|
|
+ sendStatusMessage(VPN_STATE_ERROR, ERROR_REMAIN_TIME, "No available time")
|
|
|
|
|
+ return@launch
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新通知
|
|
|
|
|
+ updateTimerNotification(currentTime)
|
|
|
|
|
+
|
|
|
|
|
+ // 发送计时更新广播
|
|
|
|
|
+ sendTimerUpdate(currentTime)
|
|
|
|
|
+
|
|
|
|
|
+ // 等待1秒
|
|
|
|
|
+ delay(1000L)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 停止计时
|
|
|
|
|
+ */
|
|
|
private fun stopTimer() {
|
|
private fun stopTimer() {
|
|
|
- if (!isTimerRunning)
|
|
|
|
|
- return
|
|
|
|
|
- VLog.i(TAG, "停止计时")
|
|
|
|
|
- isTimerRunning = false
|
|
|
|
|
|
|
+ if (timerJob != null) {
|
|
|
|
|
+ VLog.i(TAG, "停止计时")
|
|
|
|
|
+ }
|
|
|
|
|
+ timerJob?.cancel()
|
|
|
|
|
+ timerJob = null
|
|
|
isTimerPaused = false
|
|
isTimerPaused = false
|
|
|
- timerHandler.removeCallbacks(timerRunnable)
|
|
|
|
|
|
|
+ timerPausedElapsed = 0L
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 暂停计时
|
|
|
|
|
+ */
|
|
|
private fun pauseTimer() {
|
|
private fun pauseTimer() {
|
|
|
- if (isTimerRunning && !isTimerPaused) {
|
|
|
|
|
- // 记录暂停时已经过的时间
|
|
|
|
|
|
|
+ if (timerJob?.isActive == true && !isTimerPaused) {
|
|
|
val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
|
|
val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
|
|
|
timerPausedElapsed = elapsedTime
|
|
timerPausedElapsed = elapsedTime
|
|
|
|
|
|
|
|
VLog.i(TAG, "暂停计时 (已用: ${elapsedTime}ms)")
|
|
VLog.i(TAG, "暂停计时 (已用: ${elapsedTime}ms)")
|
|
|
isTimerPaused = true
|
|
isTimerPaused = true
|
|
|
- timerHandler.removeCallbacks(timerRunnable)
|
|
|
|
|
|
|
+ timerJob?.cancel()
|
|
|
|
|
+ timerJob = null
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 恢复计时
|
|
|
|
|
+ */
|
|
|
private fun resumeTimer() {
|
|
private fun resumeTimer() {
|
|
|
- if (isTimerRunning && isTimerPaused) {
|
|
|
|
|
|
|
+ if (isTimerPaused) {
|
|
|
VLog.i(TAG, "恢复计时 (之前已用: ${timerPausedElapsed}ms)")
|
|
VLog.i(TAG, "恢复计时 (之前已用: ${timerPausedElapsed}ms)")
|
|
|
|
|
|
|
|
// 恢复时重新设置基准时间,但要减去之前已经过的时间
|
|
// 恢复时重新设置基准时间,但要减去之前已经过的时间
|
|
|
timerBaseRealtime = SystemClock.elapsedRealtime() - timerPausedElapsed
|
|
timerBaseRealtime = SystemClock.elapsedRealtime() - timerPausedElapsed
|
|
|
-
|
|
|
|
|
isTimerPaused = false
|
|
isTimerPaused = false
|
|
|
- timerHandler.post(timerRunnable)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- private fun sendTimerUpdate(currentTime: Long) {
|
|
|
|
|
- try {
|
|
|
|
|
- replyMessenger?.let { messenger ->
|
|
|
|
|
- try {
|
|
|
|
|
- val msg = Message.obtain().apply {
|
|
|
|
|
- what = XRAY_MSG_REPLY_TIMER_UPDATE
|
|
|
|
|
- data = Bundle().apply {
|
|
|
|
|
- putLong("currentTime", currentTime)
|
|
|
|
|
- putInt("mode", timerMode)
|
|
|
|
|
- putBoolean("isRunning", isTimerRunning)
|
|
|
|
|
- putBoolean("isPaused", isTimerPaused)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ timerJob = lifecycleScope.launch {
|
|
|
|
|
+ while (isActive && !isTimerPaused) {
|
|
|
|
|
+ val elapsedTime = SystemClock.elapsedRealtime() - timerBaseRealtime
|
|
|
|
|
+
|
|
|
|
|
+ val currentTime = if (timerMode == TIMER_MODE_NORMAL) {
|
|
|
|
|
+ timerInitialTime + elapsedTime
|
|
|
|
|
+ } else {
|
|
|
|
|
+ timerInitialTime - elapsedTime
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (timerMode == TIMER_MODE_COUNTDOWN && currentTime <= 0) {
|
|
|
|
|
+ VLog.i(TAG, "倒计时结束 - 关闭VPN")
|
|
|
|
|
+ sendTimerUpdate(0L)
|
|
|
|
|
+ stop()
|
|
|
|
|
+ return@launch
|
|
|
}
|
|
}
|
|
|
- messenger.send(msg)
|
|
|
|
|
- VLog.i(TAG, "计时更新消息已发送: time=$currentTime, mode=$timerMode")
|
|
|
|
|
- } catch (e: DeadObjectException) {
|
|
|
|
|
- VLog.w(TAG, "Messenger已失效,清理引用", e)
|
|
|
|
|
- replyMessenger = null
|
|
|
|
|
- } catch (e: Exception) {
|
|
|
|
|
- VLog.e(TAG, "发送计时更新消息失败", e)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ updateTimerNotification(currentTime)
|
|
|
|
|
+ sendTimerUpdate(currentTime)
|
|
|
|
|
+ delay(1000L)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- } catch (e: Exception) {
|
|
|
|
|
- VLog.e(TAG, "发送计时更新失败", e)
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private fun updateNotification(currentTime: Long) {
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 发送计时更新广播
|
|
|
|
|
+ * 屏幕关闭时不发送,节省资源
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun sendTimerUpdate(currentTime: Long) {
|
|
|
|
|
+ // 屏幕关闭或应用在后台时不发送广播,节省资源
|
|
|
|
|
+ if (!isScreenOn || !isAppForeground) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ val intent = Intent(TIMER_BROADCAST).apply {
|
|
|
|
|
+ setPackage(packageName) // 必须设置包名,否则 RECEIVER_NOT_EXPORTED 接收不到
|
|
|
|
|
+ putExtra("currentTime", currentTime)
|
|
|
|
|
+ putExtra("mode", timerMode)
|
|
|
|
|
+ putExtra("isRunning", timerJob?.isActive == true)
|
|
|
|
|
+ putExtra("isPaused", isTimerPaused)
|
|
|
|
|
+ }
|
|
|
|
|
+ sendBroadcast(intent)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 更新通知显示计时
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun updateTimerNotification(currentTime: Long) {
|
|
|
val timeText = formatTime(currentTime)
|
|
val timeText = formatTime(currentTime)
|
|
|
- val modeText = if (timerMode == 0) "计时中" else "倒计时"
|
|
|
|
|
|
|
+ val modeText = if (timerMode == TIMER_MODE_NORMAL) "计时中" else "倒计时"
|
|
|
val statusText = if (isTimerPaused) "已暂停" else "运行中"
|
|
val statusText = if (isTimerPaused) "已暂停" else "运行中"
|
|
|
|
|
|
|
|
- val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
|
|
|
|
- .setContentTitle("NoMo Service $modeText")
|
|
|
|
|
- .setContentText("$timeText - $statusText")
|
|
|
|
|
- .setSmallIcon(R.drawable.ic_xvpn)
|
|
|
|
|
- .setPriority(NotificationCompat.PRIORITY_LOW)
|
|
|
|
|
- .setOngoing(true)
|
|
|
|
|
- .setShowWhen(false)
|
|
|
|
|
- .setOnlyAlertOnce(true)
|
|
|
|
|
- .build()
|
|
|
|
|
|
|
+ val notification = createConnectionNotification(
|
|
|
|
|
+ this,
|
|
|
|
|
+ NOTIFICATION_CHANNEL_ID,
|
|
|
|
|
+ "NOMO VPN $modeText",
|
|
|
|
|
+ "$timeText - $statusText",
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
val notificationManager = getSystemService(NotificationManager::class.java)
|
|
val notificationManager = getSystemService(NotificationManager::class.java)
|
|
|
- notificationManager?.notify(1, notification)
|
|
|
|
|
|
|
+ notificationManager?.notify(FOREGROUND_SERVICE_ID, notification)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查询日志状态
|
|
|
|
|
+ * QueryStatsConnectionHistory = 1 查询连接历史
|
|
|
|
|
+ * QueryStatsBandwidth = 2 带宽
|
|
|
|
|
+ * QueryStatsMaxSpeed = 3 最大速度
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun dealStat() {
|
|
|
|
|
+ val queryTypes = byteArrayOf(1, 2, 3)
|
|
|
|
|
+ val stats: String? = Ixvpn_mobile.proxyConnectorQueryStats(queryTypes)
|
|
|
|
|
+ VLog.i(Constant.TAG, "stats = $stats")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 格式化时间显示
|
|
|
|
|
+ */
|
|
|
private fun formatTime(timeMs: Long): String {
|
|
private fun formatTime(timeMs: Long): String {
|
|
|
- val totalSeconds = Math.abs(timeMs) / 1000
|
|
|
|
|
- val days = totalSeconds / 86400 // 86400 = 24 * 3600
|
|
|
|
|
|
|
+ val totalSeconds = kotlin.math.abs(timeMs) / 1000
|
|
|
|
|
+ val days = totalSeconds / 86400
|
|
|
val hours = (totalSeconds % 86400) / 3600
|
|
val hours = (totalSeconds % 86400) / 3600
|
|
|
val minutes = (totalSeconds % 3600) / 60
|
|
val minutes = (totalSeconds % 3600) / 60
|
|
|
val seconds = totalSeconds % 60
|
|
val seconds = totalSeconds % 60
|
|
|
|
|
|
|
|
- return if (days > 0) {
|
|
|
|
|
- String.format("%d 天 %02d:%02d:%02d", days, hours, minutes, seconds)
|
|
|
|
|
- } else if (hours > 0) {
|
|
|
|
|
- String.format("%02d:%02d:%02d", hours, minutes, seconds)
|
|
|
|
|
- } else {
|
|
|
|
|
- String.format("00:%02d:%02d", minutes, seconds)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private fun createNotificationChannel() {
|
|
|
|
|
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
|
|
|
- val channelName = "NoMo Service Notifications"
|
|
|
|
|
- val channelDescription = "NoMo Service is running in the background"
|
|
|
|
|
- val importance = NotificationManager.IMPORTANCE_LOW
|
|
|
|
|
-
|
|
|
|
|
- val channel = NotificationChannel(CHANNEL_ID, channelName, importance)
|
|
|
|
|
- channel.setShowBadge(false)
|
|
|
|
|
- channel.description = channelDescription
|
|
|
|
|
-
|
|
|
|
|
- val notificationManager =
|
|
|
|
|
- getSystemService(NotificationManager::class.java)
|
|
|
|
|
- notificationManager?.createNotificationChannel(channel)
|
|
|
|
|
|
|
+ return when {
|
|
|
|
|
+ days > 0 -> String.format("%d 天 %02d:%02d:%02d", days, hours, minutes, seconds)
|
|
|
|
|
+ hours > 0 -> String.format("%02d:%02d:%02d", hours, minutes, seconds)
|
|
|
|
|
+ else -> String.format("00:%02d:%02d", minutes, seconds)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- private fun createNotification() {
|
|
|
|
|
- VLog.i(TAG, "开始创建通知")
|
|
|
|
|
- try {
|
|
|
|
|
- val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
|
|
|
|
- .setContentTitle("NoMo Service Running")
|
|
|
|
|
- //.setContentText("NoMo connection is active.")
|
|
|
|
|
- .setSmallIcon(R.drawable.ic_xvpn)
|
|
|
|
|
- .setPriority(NotificationCompat.PRIORITY_MIN)
|
|
|
|
|
- .setOngoing(true)
|
|
|
|
|
- .setShowWhen(false)
|
|
|
|
|
- .setOnlyAlertOnce(true)
|
|
|
|
|
- .build()
|
|
|
|
|
-
|
|
|
|
|
- VLog.i(TAG, "通知创建成功,准备启动前台服务")
|
|
|
|
|
-
|
|
|
|
|
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
|
|
|
- try {
|
|
|
|
|
- startForeground(1, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
|
|
|
|
|
- VLog.i(TAG, "前台服务已启动 (Android 14+)")
|
|
|
|
|
- } catch (e: SecurityException) {
|
|
|
|
|
- VLog.w(TAG, "使用 SPECIAL_USE 类型失败,降级为普通前台服务", e)
|
|
|
|
|
- startForeground(1, notification)
|
|
|
|
|
- VLog.i(TAG, "前台服务已启动 (降级模式)")
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- startForeground(1, notification)
|
|
|
|
|
- VLog.i(TAG, "前台服务已启动")
|
|
|
|
|
- }
|
|
|
|
|
- } catch (e: Exception) {
|
|
|
|
|
- VLog.e(TAG, "创建通知失败", e)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- companion object {
|
|
|
|
|
- const val XRAY_MSG_START = 1
|
|
|
|
|
- const val XRAY_MSG_STOP = 2
|
|
|
|
|
- const val XRAY_MSG_REG_MESSENGER = 3
|
|
|
|
|
- const val XRAY_MSG_REPLY_STATUS = 101
|
|
|
|
|
- const val XRAY_MSG_REPLY_TIMER_UPDATE = 102
|
|
|
|
|
- const val XRAY_MSG_REPLY_STOP = 103
|
|
|
|
|
- private const val TAG = "ixvpn"
|
|
|
|
|
-
|
|
|
|
|
- private const val CHANNEL_ID: String = "nomo_channel_id"
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+}
|