refactor: achieve complete process isolation for CoreServiceManager

This commit is contained in:
2dust committed 2026-07-31 15:36:44 +08:00
1 parent 2c1efde450
commit 919dd71ea7
11 files changed
+165 -166

No files matched your search

@@ -16,18 +16,11 @@ import com.v2ray.ang.contracts.ServiceControl
import com.v2ray.ang.dto.OutboundTrafficStat
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.BrowserDialerMode
import com.v2ray.ang.extension.isComplexType
import com.v2ray.ang.extension.isNotNullEmpty
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.NotificationManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.handler.SpeedtestManager
import com.v2ray.ang.root.RootManager
import com.v2ray.ang.service.CoreProxyOnlyService
import com.v2ray.ang.service.CoreRootService
import com.v2ray.ang.service.CoreVpnService
import com.v2ray.ang.service.DialerNativeService
import com.v2ray.ang.service.DialerWebviewService
import com.v2ray.ang.service.IDialerService
@@ -62,55 +55,6 @@ object CoreServiceManager {
}
}
/**
* 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
}
try {
startContextService(context)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: ${e.message}", e)
context.toast(e.message ?: e.javaClass.simpleName)
return false
}
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) {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: startVService from ${context::class.java.simpleName}")
if (guid != null) {
MmkvManager.setSelectServer(guid)
}
try {
startContextService(context)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: ${e.message}", e)
context.toast(e.message ?: e.javaClass.simpleName)
}
}
/**
* 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.
@@ -123,98 +67,13 @@ object CoreServiceManager {
*/
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.
* @throws IllegalStateException if the core is already running, no server is selected,
* server config cannot be decoded, or server configuration is invalid.
* @throws Exception if the foreground service fails to start.
*/
@Throws(Exception::class)
private fun startContextService(context: Context) {
if (coreController.isRunning) {
LogUtil.w(AppConfig.TAG, "StartCore-Manager: Core already running")
return
}
val guid = MmkvManager.getSelectServer()
?: run {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: No server selected")
error(context.getString(R.string.app_tile_first_use))
}
val config = MmkvManager.decodeServerConfig(guid)
?: run {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Failed to decode server config")
error(context.getString(R.string.toast_config_file_invalid))
}
if (!config.configType.isComplexType()
&& !Utils.isValidUrl(config.server)
&& !Utils.isPureIpAddress(config.server.orEmpty())
) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Invalid server configuration")
error(context.getString(R.string.toast_config_file_invalid))
}
// refresh socks port when enabled dynamic socks port
SettingsManager.refreshRuntimeSocksPort()
// val result = V2rayConfigUtil.getV2rayConfig(context, guid)
// if (!result.status) error(result.errorMessage.ifBlank { "Failed to get V2Ray config" })
if (config.insecure == true && config.pinnedCA256.isNullOrEmpty()) {
context.toastError(R.string.toast_allow_insecure_deprecated)
Utils.setClipboard(context,context.getString(R.string.toast_allow_insecure_deprecated))
}
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING)) {
context.toast(R.string.toast_warning_pref_proxysharing_short)
} else {
context.toast(R.string.toast_services_start)
}
val isRootMode = SettingsManager.isRootMode()
if (isRootMode && !RootManager.isRootAvailable()) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: root mode requires root but none available")
error(context.getString(R.string.toast_root_required))
}
val intent = if (isRootMode) {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting Root service")
Intent(context.applicationContext, CoreRootService::class.java)
} else if (SettingsManager.isVpnMode()) {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting VPN service")
Intent(context.applicationContext, CoreVpnService::class.java)
} else {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Starting Proxy service")
Intent(context.applicationContext, CoreProxyOnlyService::class.java)
}
try {
ContextCompat.startForegroundService(context, intent)
} catch (e: SecurityException) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Missing permission to start foreground service", e)
throw IllegalStateException(e.message ?: e.javaClass.simpleName, e)
} catch (e: RuntimeException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
e.javaClass.name == "android.app.ForegroundServiceStartNotAllowedException"
) {
LogUtil.e(AppConfig.TAG, "StartCore-Manager: Foreground service start not allowed", e)
throw IllegalStateException(e.message ?: e.javaClass.simpleName, e)
}
throw e
}
}
/**
* 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(vpnInterface: ParcelFileDescriptor?): Boolean {
if (coreController.isRunning) {
if (isRunning()) {
LogUtil.w(AppConfig.TAG, "StartCore-Manager: Core already running")
return false
}
@@ -273,7 +132,7 @@ object CoreServiceManager {
}
coreController.startLoop(result.content, tunFd)
if (!coreController.isRunning) {
if (!isRunning()) {
error("Core failed to start")
}
@@ -308,7 +167,7 @@ object CoreServiceManager {
fun stopCoreLoop(): Boolean {
val service = getService() ?: return false
if (coreController.isRunning) {
if (isRunning()) {
CoroutineScope(Dispatchers.IO).launch {
try {
coreController.stopLoop()
@@ -372,7 +231,7 @@ object CoreServiceManager {
* Also fetches remote IP information if the delay test was successful.
*/
private fun measureV2rayDelay() {
if (coreController.isRunning == false) {
if (!isRunning()) {
return
}
@@ -511,7 +370,7 @@ object CoreServiceManager {
val serviceControl = serviceControl?.get() ?: return
when (intent?.getIntExtra("key", 0)) {
AppConfig.MSG_REGISTER_CLIENT -> {
if (coreController.isRunning) {
if (isRunning()) {
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_RUNNING, "")
} else {
MessageUtil.sendMsg2UI(serviceControl.getService(), AppConfig.MSG_STATE_NOT_RUNNING, "")
@@ -535,7 +394,7 @@ object CoreServiceManager {
LogUtil.i(AppConfig.TAG, "StartCore-Manager: Restart service")
serviceControl.stopService()
Thread.sleep(500L)
startVService(serviceControl.getService())
LauncherManager.startService(serviceControl.getService())
}
AppConfig.MSG_MEASURE_DELAY -> {
@@ -0,0 +1,129 @@
package com.v2ray.ang.core
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.extension.isComplexType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.extension.toastError
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.root.RootManager
import com.v2ray.ang.service.CoreProxyOnlyService
import com.v2ray.ang.service.CoreRootService
import com.v2ray.ang.service.CoreVpnService
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.Utils
object LauncherManager {
fun startServiceFromToggle(context: Context): Boolean {
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
context.toast(R.string.app_tile_first_use)
return false
}
try {
startContextService(context)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "LauncherManager: ${e.message}", e)
context.toast(e.message ?: e.javaClass.simpleName)
return false
}
return true
}
fun startService(context: Context, guid: String? = null) {
LogUtil.i(AppConfig.TAG, "LauncherManager: startService from ${context::class.java.simpleName}")
if (guid != null) {
MmkvManager.setSelectServer(guid)
}
try {
startContextService(context)
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "LauncherManager: ${e.message}", e)
context.toast(e.message ?: e.javaClass.simpleName)
}
}
fun stopService(context: Context) {
//context.toast(R.string.toast_services_stop)
MessageUtil.sendMsg2Service(context, AppConfig.MSG_STATE_STOP, "")
}
@Throws(Exception::class)
private fun startContextService(context: Context) {
// Note: isRunning check is removed here to avoid loading Native libraries in the UI process.
// The check is performed in CoreServiceManager when the service starts in the daemon process.
val guid = MmkvManager.getSelectServer()
?: run {
LogUtil.e(AppConfig.TAG, "LauncherManager: No server selected")
error(context.getString(R.string.app_tile_first_use))
}
val config = MmkvManager.decodeServerConfig(guid)
?: run {
LogUtil.e(AppConfig.TAG, "LauncherManager: Failed to decode server config")
error(context.getString(R.string.toast_config_file_invalid))
}
if (!config.configType.isComplexType()
&& !Utils.isValidUrl(config.server)
&& !Utils.isPureIpAddress(config.server.orEmpty())
) {
LogUtil.e(AppConfig.TAG, "LauncherManager: Invalid server configuration")
error(context.getString(R.string.toast_config_file_invalid))
}
SettingsManager.refreshRuntimeSocksPort()
if (config.insecure == true && config.pinnedCA256.isNullOrEmpty()) {
context.toastError(R.string.toast_allow_insecure_deprecated)
Utils.setClipboard(context, context.getString(R.string.toast_allow_insecure_deprecated))
}
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_PROXY_SHARING)) {
context.toast(R.string.toast_warning_pref_proxysharing_short)
} else {
context.toast(R.string.toast_services_start)
}
val isRootMode = SettingsManager.isRootMode()
if (isRootMode && !RootManager.isRootAvailable()) {
LogUtil.e(AppConfig.TAG, "LauncherManager: root mode requires root but none available")
error(context.getString(R.string.toast_root_required))
}
val intent = if (isRootMode) {
LogUtil.i(AppConfig.TAG, "LauncherManager: Starting Root service")
Intent(context.applicationContext, CoreRootService::class.java)
} else if (SettingsManager.isVpnMode()) {
LogUtil.i(AppConfig.TAG, "LauncherManager: Starting VPN service")
Intent(context.applicationContext, CoreVpnService::class.java)
} else {
LogUtil.i(AppConfig.TAG, "LauncherManager: Starting Proxy service")
Intent(context.applicationContext, CoreProxyOnlyService::class.java)
}
try {
ContextCompat.startForegroundService(context, intent)
} catch (e: SecurityException) {
LogUtil.e(AppConfig.TAG, "LauncherManager: Missing permission to start foreground service", e)
throw IllegalStateException(e.message ?: e.javaClass.simpleName, e)
} catch (e: RuntimeException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
e.javaClass.name == "android.app.ForegroundServiceStartNotAllowedException"
) {
LogUtil.e(AppConfig.TAG, "LauncherManager: Foreground service start not allowed", e)
throw IllegalStateException(e.message ?: e.javaClass.simpleName, e)
}
throw e
}
}
}
@@ -5,7 +5,7 @@ import android.content.Context
import android.content.Intent
import android.os.UserManager
import com.v2ray.ang.AppConfig
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.core.LauncherManager
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SubscriptionUpdater
import com.v2ray.ang.util.LogUtil
@@ -54,7 +54,7 @@ class BootReceiver : BroadcastReceiver() {
}
LogUtil.i(AppConfig.TAG, "BootReceiver: Starting V2Ray service")
CoreServiceManager.startVService(context)
LauncherManager.startService(context)
SubscriptionUpdater.sync(context)
}
}
@@ -5,7 +5,7 @@ import android.content.Context
import android.content.Intent
import android.text.TextUtils
import com.v2ray.ang.AppConfig
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.core.LauncherManager
import com.v2ray.ang.util.LogUtil
class TaskerReceiver : BroadcastReceiver() {
@@ -28,12 +28,12 @@ class TaskerReceiver : BroadcastReceiver() {
return
} else if (switch) {
if (guid == AppConfig.TASKER_DEFAULT_GUID) {
CoreServiceManager.startVServiceFromToggle(context)
LauncherManager.startServiceFromToggle(context)
} else {
CoreServiceManager.startVService(context, guid)
LauncherManager.startService(context, guid)
}
} else {
CoreServiceManager.stopVService(context)
LauncherManager.stopService(context)
}
} catch (e: Exception) {
LogUtil.e(AppConfig.TAG, "Error processing Tasker broadcast", e)
@@ -10,6 +10,7 @@ import android.widget.RemoteViews
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.core.LauncherManager
class WidgetProvider : AppWidgetProvider() {
/**
@@ -68,9 +69,9 @@ class WidgetProvider : AppWidgetProvider() {
super.onReceive(context, intent)
if (AppConfig.BROADCAST_ACTION_WIDGET_CLICK == intent.action) {
if (CoreServiceManager.isRunning()) {
CoreServiceManager.stopVService(context)
LauncherManager.stopService(context)
} else {
CoreServiceManager.startVServiceFromToggle(context)
LauncherManager.startServiceFromToggle(context)
}
} else if (AppConfig.BROADCAST_ACTION_ACTIVITY == intent.action) {
AppWidgetManager.getInstance(context)?.let { manager ->
@@ -31,7 +31,13 @@ class CoreProxyOnlyService : Service(), ServiceControl {
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
LogUtil.i(AppConfig.TAG, "StartCore-Proxy: Service command received")
CoreServiceManager.startCoreLoop(null)
if (!CoreServiceManager.startCoreLoop(null)) {
LogUtil.e(AppConfig.TAG, "StartCore-Proxy: Failed to start core loop")
stopSelf()
return START_NOT_STICKY
}
return START_STICKY
}
@@ -11,6 +11,7 @@ import androidx.core.content.ContextCompat
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.core.LauncherManager
import com.v2ray.ang.util.LogUtil
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.Utils
@@ -75,11 +76,11 @@ class QSTileService : TileService() {
super.onClick()
when (qsTile.state) {
Tile.STATE_INACTIVE -> {
CoreServiceManager.startVServiceFromToggle(this)
LauncherManager.startServiceFromToggle(this)
}
Tile.STATE_ACTIVE -> {
CoreServiceManager.stopVService(this)
LauncherManager.stopService(this)
}
}
}
@@ -12,7 +12,7 @@ import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.core.LauncherManager
import com.v2ray.ang.dto.entities.ProfileItem
import com.v2ray.ang.enums.EConfigType
import com.v2ray.ang.enums.PermissionType
@@ -160,7 +160,7 @@ class MainActivity : HelperBaseComponentActivity() {
private fun handleFabAction() {
if (mainViewModel.uiState.value.isRunning) {
CoreServiceManager.stopVService(this)
LauncherManager.stopService(this)
} else if (SettingsManager.isVpnMode()) {
val intent = VpnService.prepare(this)
if (intent == null) startV2Ray() else requestVpnPermission.launch(intent)
@@ -185,11 +185,11 @@ class MainActivity : HelperBaseComponentActivity() {
) {
checkAndRequestPermission(PermissionType.ACCESS_LOCAL_NETWORK) {}
}
CoreServiceManager.startVService(this)
LauncherManager.startService(this)
}
private fun restartV2Ray() {
if (mainViewModel.uiState.value.isRunning) CoreServiceManager.stopVService(this)
if (mainViewModel.uiState.value.isRunning) LauncherManager.stopService(this)
lifecycleScope.launch {
kotlinx.coroutines.delay(500)
startV2Ray()
@@ -4,6 +4,7 @@ import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.core.LauncherManager
import com.v2ray.ang.ui.base.BaseComponentActivity
class ScStartActivity : BaseComponentActivity() {
@@ -17,7 +18,7 @@ class ScStartActivity : BaseComponentActivity() {
LaunchedEffect(Unit) {
moveTaskToBack(true)
if (!CoreServiceManager.isRunning()) {
CoreServiceManager.startVServiceFromToggle(this@ScStartActivity)
LauncherManager.startServiceFromToggle(this@ScStartActivity)
}
finish()
}
@@ -4,6 +4,7 @@ import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.core.LauncherManager
import com.v2ray.ang.ui.base.BaseComponentActivity
class ScStopActivity : BaseComponentActivity() {
@@ -17,7 +18,7 @@ class ScStopActivity : BaseComponentActivity() {
LaunchedEffect(Unit) {
moveTaskToBack(true)
if (CoreServiceManager.isRunning()) {
CoreServiceManager.stopVService(this@ScStopActivity)
LauncherManager.stopService(this@ScStopActivity)
}
finish()
}
@@ -4,6 +4,7 @@ import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.v2ray.ang.core.CoreServiceManager
import com.v2ray.ang.core.LauncherManager
import com.v2ray.ang.ui.base.BaseComponentActivity
class ScSwitchActivity : BaseComponentActivity() {
@@ -17,9 +18,9 @@ class ScSwitchActivity : BaseComponentActivity() {
LaunchedEffect(Unit) {
moveTaskToBack(true)
if (CoreServiceManager.isRunning()) {
CoreServiceManager.stopVService(this@ScSwitchActivity)
LauncherManager.stopService(this@ScSwitchActivity)
} else {
CoreServiceManager.startVServiceFromToggle(this@ScSwitchActivity)
LauncherManager.startServiceFromToggle(this@ScSwitchActivity)
}
finish()
}