| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- package com.v2ray.ang.handler
- import android.app.Service
- import android.content.BroadcastReceiver
- import android.content.Context
- import android.content.Intent
- import android.content.IntentFilter
- import android.os.Build
- import android.util.Log
- import androidx.core.content.ContextCompat
- import com.v2ray.ang.AppConfig
- import com.v2ray.ang.R
- import com.v2ray.ang.dto.EConfigType
- import com.v2ray.ang.dto.ProfileItem
- import com.v2ray.ang.extension.toast
- import com.v2ray.ang.service.ServiceControl
- import com.v2ray.ang.service.V2RayProxyOnlyService
- import com.v2ray.ang.service.V2RayVpnService
- import com.v2ray.ang.util.MessageUtil
- import com.v2ray.ang.handler.PluginServiceManager
- import com.v2ray.ang.util.Utils
- import go.Seq
- import kotlinx.coroutines.CoroutineScope
- import kotlinx.coroutines.Dispatchers
- import kotlinx.coroutines.launch
- import libv2ray.CoreCallbackHandler
- import libv2ray.CoreController
- import libv2ray.Libv2ray
- import java.lang.ref.SoftReference
- object V2RayServiceManager {
- private val coreController: CoreController = Libv2ray.newCoreController(CoreCallback())
- private val mMsgReceive = ReceiveMessageHandler()
- private var currentConfig: ProfileItem? = null
- var serviceControl: SoftReference<ServiceControl>? = null
- set(value) {
- field = value
- Seq.setContext(value?.get()?.getService()?.applicationContext)
- Libv2ray.initCoreEnv(Utils.userAssetPath(value?.get()?.getService()), Utils.getDeviceIdForXUDPBaseKey())
- }
- /**
- * Starts the V2Ray service from a toggle action.
- * @param context The context from which the service is started.
- * @return True if the service was started successfully, false otherwise.
- */
- fun startVServiceFromToggle(context: Context): Boolean {
- if (MmkvManager.getSelectServer().isNullOrEmpty()) {
- context.toast(R.string.app_tile_first_use)
- return false
- }
- startContextService(context)
- return true
- }
- /**
- * Starts the V2Ray service.
- * @param context The context from which the service is started.
- * @param guid The GUID of the server configuration to use (optional).
- */
- fun startVService(context: Context, guid: String? = null) {
- if (guid != null) {
- MmkvManager.setSelectServer(guid)
- }
- startContextService(context)
- }
- /**
- * Stops the V2Ray service.
- * @param context The context from which the service is stopped.
- */
- fun stopVService(context: Context) {
- context.toast(R.string.toast_services_stop)
- MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
- }
- /**
- * Checks if the V2Ray service is running.
- * @return True if the service is running, false otherwise.
- */
- fun isRunning() = coreController.isRunning
- /**
- * Gets the name of the currently running server.
- * @return The name of the running server.
- */
- fun getRunningServerName() = currentConfig?.remarks.orEmpty()
- /**
- * Starts the context service for V2Ray.
- * Chooses between VPN service or Proxy-only service based on user settings.
- * @param context The context from which the service is started.
- */
- private fun startContextService(context: Context) {
- if (coreController.isRunning) {
- return
- }
- val guid = MmkvManager.getSelectServer() ?: return
- val config = MmkvManager.decodeServerConfig(guid) ?: return
- if (config.configType != EConfigType.CUSTOM
- && !Utils.isValidUrl(config.server)
- && !Utils.isPureIpAddress(config.server.orEmpty())
- ) return
- // val result = V2rayConfigUtil.getV2rayConfig(context, guid)
- // if (!result.status) return
- if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING) == true) {
- context.toast(R.string.toast_warning_pref_proxysharing_short)
- } else {
- context.toast(R.string.toast_services_start)
- }
- val intent = if ((MmkvManager.decodeSettingsString(AppConfig.PREF_MODE) ?: AppConfig.VPN) == AppConfig.VPN) {
- Intent(context.applicationContext, V2RayVpnService::class.java)
- } else {
- Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
- }
- if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
- context.startForegroundService(intent)
- } else {
- context.startService(intent)
- }
- }
- /**
- * Refer to the official documentation for [registerReceiver](https://developer.android.com/reference/androidx/core/content/ContextCompat#registerReceiver(android.content.Context,android.content.BroadcastReceiver,android.content.IntentFilter,int):
- * `registerReceiver(Context, BroadcastReceiver, IntentFilter, int)`.
- * Starts the V2Ray core service.
- */
- fun startCoreLoop(): Boolean {
- if (coreController.isRunning) {
- return false
- }
- val service = getService() ?: return false
- val guid = MmkvManager.getSelectServer() ?: return false
- val config = MmkvManager.decodeServerConfig(guid) ?: return false
- val result = V2rayConfigManager.getV2rayConfig(service, guid)
- if (!result.status)
- return false
- try {
- val mFilter = IntentFilter(AppConfig.BROADCAST_ACTION_SERVICE)
- mFilter.addAction(Intent.ACTION_SCREEN_ON)
- mFilter.addAction(Intent.ACTION_SCREEN_OFF)
- mFilter.addAction(Intent.ACTION_USER_PRESENT)
- ContextCompat.registerReceiver(service, mMsgReceive, mFilter, Utils.receiverFlags())
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to register broadcast receiver", e)
- return false
- }
- currentConfig = config
- try {
- coreController.startLoop(result.content)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to start Core loop", e)
- return false
- }
- if (coreController.isRunning == false) {
- MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
- NotificationManager.cancelNotification()
- return false
- }
- try {
- MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
- NotificationManager.showNotification(currentConfig)
- NotificationManager.startSpeedNotification(currentConfig)
- PluginServiceManager.runPlugin(service, config, result.socksPort)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to startup service", e)
- return false
- }
- return true
- }
- /**
- * Stops the V2Ray core service.
- * Unregisters broadcast receivers, stops notifications, and shuts down plugins.
- * @return True if the core was stopped successfully, false otherwise.
- */
- fun stopCoreLoop(): Boolean {
- val service = getService() ?: return false
- if (coreController.isRunning) {
- CoroutineScope(Dispatchers.IO).launch {
- try {
- coreController.stopLoop()
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to stop V2Ray loop", e)
- }
- }
- }
- MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_STOP_SUCCESS, "")
- NotificationManager.cancelNotification()
- try {
- service.unregisterReceiver(mMsgReceive)
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to unregister broadcast receiver", e)
- }
- PluginServiceManager.stopPlugin()
- return true
- }
- /**
- * Queries the statistics for a given tag and link.
- * @param tag The tag to query.
- * @param link The link to query.
- * @return The statistics value.
- */
- fun queryStats(tag: String, link: String): Long {
- return coreController.queryStats(tag, link)
- }
- /**
- * Measures the connection delay for the current V2Ray configuration.
- * Tests with primary URL first, then falls back to alternative URL if needed.
- * Also fetches remote IP information if the delay test was successful.
- */
- private fun measureV2rayDelay() {
- if (coreController.isRunning == false) {
- return
- }
- CoroutineScope(Dispatchers.IO).launch {
- val service = getService() ?: return@launch
- var time = -1L
- var errorStr = ""
- try {
- time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to measure delay with primary URL", e)
- errorStr = e.message?.substringAfter("\":") ?: "empty message"
- }
- if (time == -1L) {
- try {
- time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to measure delay with alternative URL", e)
- errorStr = e.message?.substringAfter("\":") ?: "empty message"
- }
- }
- val result = if (time >= 0) {
- service.getString(R.string.connection_test_available, time)
- } else {
- service.getString(R.string.connection_test_error, errorStr)
- }
- MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, result)
- // Only fetch IP info if the delay test was successful
- if (time >= 0) {
- SpeedtestManager.getRemoteIPInfo()?.let { ip ->
- MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip")
- }
- }
- }
- }
- /**
- * Gets the current service instance.
- * @return The current service instance, or null if not available.
- */
- private fun getService(): Service? {
- return serviceControl?.get()?.getService()
- }
- /**
- * Core callback handler implementation for handling V2Ray core events.
- * Handles startup, shutdown, socket protection, and status emission.
- */
- private class CoreCallback : CoreCallbackHandler {
- /**
- * Called when V2Ray core starts up.
- * @return 0 for success, any other value for failure.
- */
- override fun startup(): Long {
- return 0
- }
- /**
- * Called when V2Ray core shuts down.
- * @return 0 for success, any other value for failure.
- */
- override fun shutdown(): Long {
- val serviceControl = serviceControl?.get() ?: return -1
- return try {
- serviceControl.stopService()
- 0
- } catch (e: Exception) {
- Log.e(AppConfig.TAG, "Failed to stop service in callback", e)
- -1
- }
- }
- /**
- * Called when V2Ray core emits status information.
- * @param l Status code.
- * @param s Status message.
- * @return Always returns 0.
- */
- override fun onEmitStatus(l: Long, s: String?): Long {
- return 0
- }
- }
- /**
- * Broadcast receiver for handling messages sent to the service.
- * Handles registration, service control, and screen events.
- */
- private class ReceiveMessageHandler : BroadcastReceiver() {
- /**
- * Handles received broadcast messages.
- * Processes service control messages and screen state changes.
- * @param ctx The context in which the receiver is running.
- * @param intent The intent being received.
- */
- override fun onReceive(ctx: Context?, intent: Intent?) {
- val serviceControl = serviceControl?.get() ?: return
- when (intent?.getIntExtra("key", 0)) {
- AppConfig.MSG_REGISTER_CLIENT -> {
- if (coreController.isRunning) {
- MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
- } else {
- MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
- }
- }
- AppConfig.MSG_UNREGISTER_CLIENT -> {
- // nothing to do
- }
- AppConfig.MSG_STATE_START -> {
- // nothing to do
- }
- AppConfig.MSG_STATE_STOP -> {
- Log.i(AppConfig.TAG, "Stop Service")
- serviceControl.stopService()
- }
- AppConfig.MSG_STATE_RESTART -> {
- Log.i(AppConfig.TAG, "Restart Service")
- serviceControl.stopService()
- Thread.sleep(500L)
- startVService(serviceControl.getService())
- }
- AppConfig.MSG_MEASURE_DELAY -> {
- measureV2rayDelay()
- }
- }
- when (intent?.action) {
- Intent.ACTION_SCREEN_OFF -> {
- Log.i(AppConfig.TAG, "SCREEN_OFF, stop querying stats")
- NotificationManager.stopSpeedNotification(currentConfig)
- }
- Intent.ACTION_SCREEN_ON -> {
- Log.i(AppConfig.TAG, "SCREEN_ON, start querying stats")
- NotificationManager.startSpeedNotification(currentConfig)
- }
- }
- }
- }
- }
|